packages feed

hgettext-0.1.40: src-exe/hgettext.hs

module Main (main) where

import           Control.DeepSeq
import           Control.Exception
import           Control.Monad
import           Data.Generics.Uniplate.Data
import           Data.List.Split             (splitOn)
import qualified Data.Map                    as Map
import qualified Data.Set                    as Set
import           Data.Version                (showVersion)
import           Language.Preprocessor.Cpphs as C
import qualified Language.Haskell.Exts       as H
import           System.Console.GetOpt
import           System.Environment
import           System.Exit
import           System.IO                   (IOMode(WriteMode), hPutStr, hSetEncoding, utf8, withFile)
import           System.IO.Extra             (readFileUTF8)

import           Paths_hgettext              (version)

data Options = Options
  { outputFile   :: FilePath
  , keyword      :: String
  , extensions   :: [H.Extension]
  , cpp          :: Bool
  , cpp_defs     :: [(String, String)]
  , printVersion :: Bool
  } deriving Show

options :: [OptDescr (Options->Options)]
options =
  [ Option ['o'] ["output"]
           (ReqArg (\o opts -> opts {outputFile = o}) "FILE")
           "write output to specified file"
  , Option ['d'] ["default-domain"]
           (ReqArg (\d opts -> opts {outputFile = d ++ ".po"}) "NAME")
           "use NAME.po instead of messages.po"
  , Option ['k'] ["keyword"]
           (ReqArg (\d opts -> opts {keyword = d}) "WORD")
           "function name, in which wrapped searched words"
  , Option ['e'] ["lang-exts"]
           (ReqArg (\es opts -> opts {extensions = map (\e -> H.parseExtension e) (splitOn "," es)}) "EXTENSION...")
           "language extensions to enable/disable when parsing input (prefix \"No\" to an extension to disable it)"
  , Option [] ["cpp"]
           (NoArg (\opts -> opts {cpp = True}))
           "do the C pre-processing"
  , Option [] ["cpp-defs"]
           (ReqArg (\defs opts -> opts {cpp_defs = map (\def -> let l = splitOn "=" def in (head l, last l)) (splitOn "," defs)}) "IDENTIFIER=VALUE...")
           "C pre-processing defines list"
  , Option [] ["version"]
           (NoArg (\opts -> opts {printVersion = True}))
           "print version of hgettexts"
  ]

defaultOptions :: Options
defaultOptions = Options "messages.po" "__" [] False [] False

parseArgs :: [String] -> IO (Options, [String])
parseArgs args =
    case getOpt Permute options args of
      (o, n, []) -> return (foldl (flip id) defaultOptions o, n)
      (_, _, errs) -> ioError (userError (concat errs ++ usageInfo header options))
    where header = "Usage: hgettext [OPTION] [INPUTFILE] ..."


toTranslate :: String -> H.Module H.SrcSpanInfo -> [(Int, String)]
toTranslate f z = [ (H.srcSpanStartLine (H.srcInfoSpan loc), s)
                  | H.App _ (H.Var _
                              (H.UnQual _
                                (H.Ident  _ x)))
                            (H.Lit _
                              (H.String loc s _slit))
                    <- universeBi z :: [H.Exp H.SrcSpanInfo]
                  , x == f]

showStringLit :: String -> String
showStringLit s0 = '"' : concatMap showChar s0 ++ "\""
    where
      showChar '"' = "\\\""
      showChar '\\' = "\\\\"
      showChar '\n' = "\\n"
      showChar c = return c

formatMessage :: String -> [(FilePath, Int)] -> String
formatMessage s locs = unlines $
                       map (uncurry formatLoc) locs ++
                       [ "msgid " ++ (showStringLit s)
                       , "msgstr \"\""
                       , ""
                       ]
  where
    formatLoc src l = "#: " ++ src ++ ":" ++ (show l)


writePOTFile :: [String] -> String
writePOTFile l = concat $ [potHeader] ++ l
    where potHeader = unlines ["# Translation file",
                               "",
                               "msgid \"\"",
                               "msgstr \"\"",
                               "",
                               "\"Project-Id-Version: PACKAGE VERSION\\n\"",
                               "\"Report-Msgid-Bugs-To: \\n\"",
                               "\"POT-Creation-Date: 2009-01-13 06:05-0800\\n\"",
                               "\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"",
                               "\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"",
                               "\"Language-Team: LANGUAGE <LL@li.org>\\n\"",
                               "\"MIME-Version: 1.0\\n\"",
                               "\"Content-Type: text/plain; charset=UTF-8\\n\"",
                               "\"Content-Transfer-Encoding: 8bit\\n\"",
                               ""]

writeFileUtf8 :: FilePath -> String -> IO ()
writeFileUtf8 fp content =
  withFile fp WriteMode $ \h -> do
    hSetEncoding h utf8
    hPutStr h content

process :: Options -> [FilePath] -> IO ()
process Options{printVersion = True} _ =
    putStrLn $ "hgettext, version " ++ (showVersion version)
process opts fl = do
    dat <- forM fl $ \fn -> do
      m <- readSource fn
      evaluate $ force [ (s,(fn,loc)) | (loc,s) <- toTranslate (keyword opts) m ]

    let entries = Map.fromListWith Set.union [ (s,Set.singleton (fn,loc)) | d <- dat, (s,(fn,loc)) <- d ]

    writeFileUtf8 (outputFile opts) $ do
      writePOTFile [ formatMessage s (Set.toList locs) | (s,locs) <- Map.toList entries ]
  where
    readSource f = do
      let rf = if f == "-" then "<stdin>" else f
      c <- (if f == "-" then getContents else readFileUTF8 f) >>= if cpp opts then C.runCpphs (C.defaultCpphsOptions {C.defines = cpp_defs opts}) rf else return
      case H.parseFileContentsWithMode (H.defaultParseMode {H.parseFilename = rf, H.extensions = extensions opts}) c of
        H.ParseFailed loc msg -> do
          putStrLn (concat [ rf, ":", show (H.srcLine loc), ":", show (H.srcColumn loc), ": error: ", msg ])
          exitFailure
        H.ParseOk m -> return m

main :: IO ()
main = getArgs >>= parseArgs >>= uncurry process