Skip to content

Commit e50b324

Browse files
authored
Merge pull request #3 from ffaf1/exe
Various improvements to `hgettext`
2 parents f11ceaa + e23800b commit e50b324

File tree

17 files changed

+278
-114
lines changed

17 files changed

+278
-114
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Next
2+
----
3+
4+
- hgettext: added support for Unicode characters.

exe/Main.hs

Lines changed: 84 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,56 @@
1-
-- (C) vasylp https://github.com/vasylp/hgettext/blob/master/src/hgettext.hs
2-
3-
import qualified Language.Haskell.Exts as H
4-
5-
import System.Environment
6-
import System.Console.GetOpt
7-
import Data.Time
8-
import System.Locale hiding (defaultTimeLocale)
9-
10-
import Data.Generics.Uniplate.Data
11-
12-
-- import Distribution.Simple.PreProcess.Unlit
13-
14-
import Data.List
15-
import Data.Char
16-
import Data.Ord
17-
import Data.Function (on)
18-
import System.FilePath
19-
20-
import Data.Version (showVersion)
21-
version = undefined
22-
-- import Paths_haskell_gettext (version)
23-
24-
data Options = Options {
25-
outputFile :: String,
26-
keywords :: [String],
27-
printVersion :: Bool
28-
} deriving Show
29-
30-
options :: [OptDescr (Options->Options)]
31-
options =
32-
[
33-
Option ['o'] ["output"]
34-
(ReqArg (\o opts -> opts {outputFile = o}) "FILE")
35-
"write output to specified file",
36-
Option ['d'] ["default-domain"]
37-
(ReqArg (\d opts -> opts {outputFile = d ++ ".po"}) "NAME")
38-
"use NAME.po instead of messages.po",
39-
Option ['k'] ["keyword"]
40-
(ReqArg (\d opts -> opts {keywords = d: keywords opts}) "WORD")
41-
"function names, in which searched words are wrapped. Can be used multiple times, for multiple funcitons",
42-
Option [] ["version"]
43-
(NoArg (\opts -> opts {printVersion = True}))
44-
"print version of hgettexts"
45-
]
46-
47-
48-
defaultOptions = Options "messages.po" ["__", "lprintf"] False
49-
50-
parseArgs :: [String] -> IO (Options, [String])
51-
parseArgs args =
52-
case getOpt Permute options args of
53-
(o, n, []) -> return (foldl (flip id) defaultOptions o, n)
54-
(_, _, errs) -> ioError (userError (concat errs ++ usageInfo header options))
55-
where header = "Usage: hgettext [OPTION] [INPUTFILE] ..."
56-
57-
58-
toTranslate :: [String] -> H.ParseResult (H.Module H.SrcSpanInfo) -> [(H.SrcSpanInfo, String)]
59-
toTranslate f (H.ParseOk z) = nub [ (loc, s) | H.App _ (H.Var _ (H.UnQual _ (H.Ident _ x))) (H.Lit _ (H.String loc s _)) <- universeBi z, x `elem` f]
60-
toTranslate _ _ = []
61-
62-
-- Create list of messages from a single file
63-
formatMessages :: String -> [(H.SrcSpanInfo, String)] -> String
64-
formatMessages path l = concat $ map potEntry $ nubBy ((==) `on` snd) $ sortBy (comparing snd) l
65-
where potEntry (l, s) = unlines [
66-
"#: " ++ showSrc l,
67-
"msgid " ++ (show s),
68-
"msgstr \"\"",
69-
""
70-
]
71-
showSrc l = path ++ ":" ++ show (H.srcSpanStartLine (H.srcInfoSpan l)) ++ ":" ++ show (H.srcSpanStartColumn (H.srcInfoSpan l))
1+
-- Originally copied from https://github.com/vasylp/hgettext/
2+
-- © 2009 Vasyl Pasternak, BSD-3-Clause.
3+
4+
import Options
5+
import Paths_haskell_gettext (version)
6+
7+
import qualified Data.Generics.Uniplate.Data as G
8+
import qualified Data.Function as F
9+
import qualified Data.List as L
10+
import qualified Data.Ord as O
11+
import qualified Data.Text as T
12+
import qualified Data.Time as TM
13+
import qualified Data.Text.IO.Utf8 as Utf8
14+
import qualified Data.Version as V
15+
import qualified Language.Haskell.Exts as H
16+
17+
18+
main :: IO ()
19+
main = do
20+
opts <- parseOptions
21+
process opts
22+
23+
process :: Options -> IO ()
24+
process Options{printVersion = True} =
25+
putStrLn $ "hgettext (from haskell-gettext), version " ++
26+
(V.showVersion version)
27+
process opts
28+
| null (inputFiles opts) = do
29+
putStrLn "hgettext: missing arguments"
30+
| otherwise = do
31+
t <- mapM read' (inputFiles opts)
32+
pot <- formatPotFile $
33+
map (\(n,c) -> formatMessages n $
34+
toTranslate (keywords opts) c) t
35+
Utf8.writeFile (outputFile opts) (T.pack pot)
36+
where
37+
read' :: FilePath ->
38+
IO (String, H.ParseResult (H.Module H.SrcSpanInfo))
39+
read' "-" = getContents >>= \c -> return ("-", H.parseFileContents c)
40+
read' f = H.parseFile f >>= \m -> return (f, m)
41+
42+
-------------------------------------------------------------------------------
43+
-- Write
7244

7345

7446
formatPotFile :: [String] -> IO String
75-
formatPotFile lines = do
76-
time <- getZonedTime
77-
let timeStr = formatTime defaultTimeLocale "%F %R%z" time
47+
formatPotFile ls = do
48+
time <- TM.getZonedTime
49+
let timeStr = TM.formatTime TM.defaultTimeLocale "%F %R%z" time
7850
let header = formatPotHeader timeStr
79-
return $ concat $ header: lines
51+
return $ concat (header:ls)
8052
where
53+
formatPotHeader :: String -> String
8154
formatPotHeader timeStr =
8255
unlines ["# Translation file",
8356
"",
@@ -95,18 +68,40 @@ formatPotFile lines = do
9568
"\"Content-Transfer-Encoding: 8bit\\n\"",
9669
""]
9770

98-
process :: Options -> [String] -> IO ()
99-
process Options{printVersion = True} _ =
100-
putStrLn $ "hgettext, version " ++ (showVersion version)
101-
102-
process opts fl = do
103-
t <- mapM read' fl
104-
pot <- formatPotFile $ map (\(n,c) -> formatMessages n $ toTranslate (keywords opts) c) t
105-
writeFile (outputFile opts) pot
106-
where read' "-" = getContents >>= \c -> return ("-", H.parseFileContents c)
107-
read' f = H.parseFile f >>= \m -> return (f, m)
108-
109-
main =
110-
getArgs >>= parseArgs >>= uncurry process
111-
71+
-- Create list of messages from a single file.
72+
formatMessages :: String -> [(H.SrcSpanInfo, String)] -> String
73+
formatMessages path l =
74+
let sorted = L.sortBy (O.comparing snd) l
75+
nubbed = L.nubBy ((==) `F.on` snd) sorted
76+
in concatMap potEntry nubbed
77+
where
78+
potEntry :: (H.SrcSpanInfo, String) -> String
79+
potEntry (wl, s) = unlines
80+
["#: " ++ showSrc wl,
81+
"msgid " ++ (showStringC s),
82+
"msgstr \"\"",
83+
""]
84+
85+
showSrc :: H.SrcSpanInfo -> String
86+
showSrc wl = path ++ ":" ++
87+
show (H.srcSpanStartLine (H.srcInfoSpan wl)) ++ ":" ++
88+
show (H.srcSpanStartColumn (H.srcInfoSpan wl))
89+
90+
toTranslate :: [String] -> H.ParseResult (H.Module H.SrcSpanInfo) ->
91+
[(H.SrcSpanInfo, String)]
92+
toTranslate f (H.ParseOk z) =
93+
L.nub [(loc, s) |
94+
H.App _ (H.Var _ (H.UnQual _ (H.Ident _ x)))
95+
(H.Lit _ (H.String loc s _)) <- G.universeBi z,
96+
x `elem` f]
97+
toTranslate _ _ = []
11298

99+
-- Escape a string in a C-like fashion,
100+
-- see https://www.ibm.com/docs/en/i/7.4?topic=literals-string
101+
showStringC :: String -> String
102+
showStringC s0 = '"' : concatMap showCharC s0 ++ "\""
103+
where
104+
showCharC '"' = "\\\""
105+
showCharC '\\' = "\\\\"
106+
showCharC '\n' = "\\n"
107+
showCharC c = return c

exe/Options.hs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module Options (
2+
Options(..),
3+
parseOptions
4+
)
5+
where
6+
7+
import Options.Applicative
8+
9+
import qualified System.FilePath as FP
10+
11+
data Options = Options {
12+
inputFiles :: [FilePath],
13+
outputFile :: FilePath,
14+
keywords :: [String],
15+
printVersion :: Bool
16+
} deriving Show
17+
18+
parseOptions :: IO Options
19+
parseOptions = execParser infoOpts
20+
21+
-------------------------------------------------------------------------------
22+
-- Parsers/properties
23+
24+
infoOpts :: ParserInfo Options
25+
infoOpts = info (options <**> helper)
26+
( fullDesc
27+
<> progDesc "Extract translatable strings from Haskell source files."
28+
<> header "hgettext (from haskell-gettext)" )
29+
30+
options :: Parser Options
31+
options = Options <$> inputs <*> outfile <*> kwsDef <*> version
32+
where
33+
kwsDef = ("__" :) <$> many keyword
34+
35+
inputs :: Parser [FilePath]
36+
inputs = many (strArgument (metavar "PATH..."))
37+
38+
outfile :: Parser FilePath
39+
outfile = output <|> ((FP.<.> "po") <$> defaultDomain) <|> pure "messages.po"
40+
where
41+
output :: Parser FilePath
42+
output = strOption
43+
( long "output"
44+
<> short 'o'
45+
<> metavar "FILE"
46+
<> help "write output to specified file" )
47+
48+
defaultDomain :: Parser FilePath
49+
defaultDomain = strOption
50+
( long "default-domain"
51+
<> short 'd'
52+
<> metavar "NAME"
53+
<> help "use NAME.po instead of messages.po" )
54+
55+
keyword :: Parser String
56+
keyword = strOption
57+
( long "keyword"
58+
<> short 'k'
59+
<> metavar "WORD"
60+
<> help "function names, in which searched words are \
61+
\wrapped. Can be used multiple times, for multiple \
62+
\funcitons" )
63+
64+
version :: Parser Bool
65+
version = switch
66+
( long "version"
67+
<> short 'v'
68+
<> help "print version of hgettext" )

haskell-gettext.cabal

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ maintainer: [email protected]
3434
copyright: © 2017–2019 Ilya Portnov
3535
category: Text
3636
build-type: Simple
37-
extra-doc-files: README.md
37+
extra-doc-files: README.md, NEWS
3838
extra-source-files: examples/gmodump.hs
3939
examples/gmotest.hs
4040

4141
common shared
42-
build-depends: base == 4.*
42+
build-depends: base == 4.*,
43+
text >= 2.1.2 && < 2.2,
4344
ghc-options: -Wall
4445
default-language: Haskell2010
4546

@@ -54,17 +55,20 @@ library
5455
containers >=0.5 && < 0.7,
5556
mtl >= 2.2.1 && < 2.4,
5657
parsec >= 3.1.11 && < 3.2,
57-
text >= 1.2 && < 2.2,
5858
time >=1.4 && < 1.13,
5959
transformers >=0.3 && < 0.7
6060
hs-source-dirs: lib/
6161

6262
executable hgettext
6363
import: shared
6464
main-is: Main.hs
65+
autogen-modules: Paths_haskell_gettext
66+
other-modules: Options,
67+
Paths_haskell_gettext
6568
build-depends: filepath >= 1.4 && < 1.6,
6669
haskell-src-exts >= 1.18 && < 1.24,
67-
old-locale >= 1.0 && < 1.1
70+
old-locale >= 1.0 && < 1.1,
71+
optparse-applicative >= 0.18.1 && < 0.19,
6872
time >= 1.5.0 && < 1.13,
6973
uniplate >= 1.6.12 && < 1.7,
7074
hs-source-dirs: exe/

lib/Data/Gettext.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ cgettext :: Catalog
119119
-> B.ByteString -- ^ Message context (@msgctxt@ line in @po@ file)
120120
-> B.ByteString -- ^ Original string
121121
-> T.Text
122-
cgettext gmo context key = gettext gmo (context `B.append` "\4" `B.append` key)
122+
cgettext gmo ctx key = gettext gmo (ctx `B.append` "\4" `B.append` key)
123123

124124
-- | Translate a string and select correct plural form.
125125
-- Original single form must be defined in @po@ file in @msgid@ line.
@@ -140,8 +140,8 @@ cngettext :: Catalog
140140
-> B.ByteString -- ^ Plural form in original language
141141
-> Int -- ^ Number
142142
-> T.Text
143-
cngettext gmo context single plural n =
144-
ngettext' gmo (context `B.append` "\4" `B.append` single `B.append` "\0" `B.append` plural) n
143+
cngettext gmo ctx single plural n =
144+
ngettext' gmo (ctx `B.append` "\4" `B.append` single `B.append` "\0" `B.append` plural) n
145145

146146
-- | Variant of @ngettext@ for case when for some reason there is only
147147
-- @msgid@ defined in @po@ file, and no @msgid_plural@, but there are some @msgstr[n]@.

lib/Data/Gettext/GmoFile.hs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,3 @@ parseGmo = do
6969
fOriginals = origs,
7070
fTranslations = trans,
7171
fData = undefined }
72-
73-
withGmoFile :: FilePath -> (GmoFile -> IO a) -> IO a
74-
withGmoFile path go = do
75-
content <- L.readFile path
76-
let gmo = (runGet parseGmo content) {fData = content}
77-
result <- go gmo
78-
return result
79-

lib/Data/Gettext/Parsers.hs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ type Headers = [Header]
3838
pHeader :: Parser Header
3939
pHeader = do
4040
name <- (many1 $ alphaNum <|> char '-') <?> "Header name"
41-
char ':'
42-
many $ oneOf " \t"
41+
_ <- char ':'
42+
_ <- many $ oneOf " \t"
4343
value <- many $ noneOf "\r\n"
4444
return (T.pack name, T.pack value)
4545

@@ -51,8 +51,8 @@ pHeaders = pHeader `sepEndBy` newline
5151
-- NB: for now this function does not use Parsec.
5252
parseHeaders :: T.Text -> Either String Headers
5353
parseHeaders t = do
54-
let lines = filter (not . T.null) $ T.splitOn "\n" t
55-
forM lines $ \line ->
54+
let ls = filter (not . T.null) $ T.splitOn "\n" t
55+
forM ls $ \line ->
5656
case T.splitOn ": " line of
5757
[name, value] -> return (name, value)
5858
(name:values) -> return (name, T.intercalate ": " values)
@@ -91,7 +91,7 @@ pTernary :: Parser (Expr, Expr)
9191
pTernary = do
9292
reservedOp "?"
9393
true <- pExpr
94-
colon
94+
_ <- colon
9595
false <- pExpr
9696
return (true, false)
9797

@@ -100,11 +100,11 @@ pTernary = do
100100
-- starting from @nplurals=@.
101101
pPlural :: Parser (Int, Expr)
102102
pPlural = do
103-
symbol "nplurals"
103+
_ <- symbol "nplurals"
104104
reservedOp "="
105105
n <- natural
106-
semi
107-
symbol "plural"
106+
_ <- semi
107+
_ <- symbol "plural"
108108
reservedOp "="
109109
expr <- pExpr
110110
return (n, expr)

lib/Data/Gettext/Plural.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,4 @@ eval (If cond true false) n =
7777
eval (Binary op x y) n =
7878
evalOp op (eval x n) (eval y n)
7979
eval (Negate x) n = negate $ eval x n
80-
80+
eval (Not x) n = if eval x n == 0 then 1 else eval x n

0 commit comments

Comments
 (0)