packages feed

gettext-th-0.2.0.0: src/I18N/Gettext/TH.hs

module I18N.Gettext.TH
(gettext, __)
where

import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax

import Instances.TH.Lift()
import System.IO.Unsafe
import System.Directory
import Data.IORef
import Control.Monad
import Data.Bifunctor
import Data.Char (isSpace)
import Data.List
import Data.Set (Set)
import qualified Data.Set as S

import qualified Data.ByteString as B
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Text.Encoding (encodeUtf8)

import qualified Data.Gettext as G
import Data.Gettext (Catalog, loadCatalog)
import System.FilePath.Posix
import System.IO

{-# NOINLINE knownMsgs #-}
knownMsgs :: IORef (Set String)
knownMsgs = unsafePerformIO $ newIORef S.empty

potFileName :: FilePath
potFileName = "po/messages.pot"


-- poFileName :: FilePath
-- poFileName = replaceExtension potFileName ".po"

moFileName :: FilePath
moFileName = replaceExtension potFileName ".mo"

{-# NOINLINE catalog #-}
catalog :: Catalog
catalog = unsafePerformIO $ loadCatalog moFileName


header :: String
header = unlines [
  "# SOME DESCRIPTIVE TITLE.",
  "# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER",
  "# This file is distributed under the same license as the PACKAGE package.",
  "# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.",
  "#",
  "#: hello.c:140",
  "#, fuzzy",
  "msgid \"\"",
  "msgstr \"\"",
  "\"Project-Id-Version: PACKAGE VERSION\\n\"",
  "\"Report-Msgid-Bugs-To: \\n\"",
  "\"POT-Creation-Date: 2022-08-03 07:51+0200\\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\"",
  "\"Language: \\n\"",
  "\"MIME-Version: 1.0\\n\"",
  "\"Content-Type: text/plain; charset=utf-8\\n\"",
  "\"Content-Transfer-Encoding: 8bit\\n\""
  ]




writeFileUtf8 :: FilePath -> IOMode -> String -> IO ()
writeFileUtf8 f mode txt = withFile f mode (\ hdl -> do
                                               hSetEncoding hdl utf8
                                               hPutStr hdl txt)  

createPotFile :: Q ()
createPotFile = do
  fn <- runIO $ do
      createDirectoryIfMissing True (takeDirectory potFileName)
      potE <- doesFileExist potFileName    
      when potE $
        renameFile potFileName (potFileName ++ ".bak")

      writeFileUtf8 potFileName WriteMode header
      makeAbsolute moFileName
  addDependentFile fn


packStr :: String -> B.ByteString
packStr = encodeUtf8 . T.pack

gettextQ  :: String -> Q Exp
gettextQ str = do
  kmsgs <- runIO $ do
     kmsgs <- readIORef knownMsgs
     writeIORef knownMsgs (S.insert str kmsgs)
     return kmsgs

  when (S.null kmsgs) createPotFile
  when (str `S.notMember` kmsgs) $ do
    loc <- location
    runIO $ writeFileUtf8 potFileName AppendMode $ unlines $ poEntry loc str
  let trans = TL.toStrict $ G.gettext catalog (packStr str)
  [| trans |]

quote :: String -> String
quote s =  '"':escape s
  where escape [] = "\""
        escape ('"':s') = '\\':'"':escape s'
        escape ('\n':s') = '\\':'n':escape s'
        escape ('\r':s') = escape s'
        escape (c:s') = c:escape s'
        

poEntry :: Loc -> String -> [String]
poEntry loc msg = [
      "",
      "#: " ++ (loc_filename loc) ++ ":0", -- TODO line nr or char pos
      "msgid " ++ quote msg,
      "msgstr " ++ quote msg 
      ]

gettextsDecs  :: String -> Q [Dec]
gettextsDecs str = do
  let msgs = map splitKeyMsg $ parseLines str  
  kmsgs <- runIO $ do
     kmsgs <- readIORef knownMsgs
     writeIORef knownMsgs (foldl' (\ acc (_, msg) -> msg `S.insert` acc) kmsgs msgs)
     return kmsgs
  when (S.null kmsgs) createPotFile
  loc <- location

  runIO $ writeFileUtf8 potFileName AppendMode $ unlines $ concat [ poEntry loc msg | (_, msg) <- msgs, msg `S.notMember` kmsgs ]    

  forM msgs $ \ (key, msg) ->
              let trans = TL.toStrict $ G.gettext catalog (packStr msg) in do
                 funD (mkName key) [clause [] (normalB [| trans |]) []]
                 

         

parseLines :: String -> [String]
parseLines text = go [] (lines text)
   where go acc [] = reverse acc
         go acc (('#':_):lines') = go acc lines'
         go acc (line:lines') =
           if all isSpace line then go acc lines'
             else collect (join acc) [line] lines'
         collect :: ([String] -> [String]) -> [String] -> [String] -> [String]
         collect j cl [] = go (j cl) []
         collect j cl ([]:t) = go (j cl) t
         collect j cl lines'@((c:d):t) =
                   if isSpace c then collect j ((dropWhile isSpace d):cl) t
                     else go (j cl) lines'
         join acc cl = (intercalate "\n" $ reverse cl):acc

splitKeyMsg :: String -> (String, String)        
splitKeyMsg line =  bimap trim (trim . tail) $ span (/= ':') line 
  

trim :: String -> String
trim = f . f
   where f = reverse . dropWhile isSpace


gettext :: QuasiQuoter
gettext = QuasiQuoter
  { quoteExp  = gettextQ
  , quotePat  = error "Usage as a pattern is not supported"
  , quoteType = error "Usage as a type is not supported"
  , quoteDec = gettextsDecs
  }

__ :: QuasiQuoter
__ = gettext