2 {-# LANGUAGE ExistentialQuantification #-}
3 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE MultiParamTypeClasses #-}
5 {-# LANGUAGE TemplateHaskell #-}
6 {-# LANGUAGE TypeFamilies #-}
7 {-# LANGUAGE TypeSynonymInstances #-}
9 -----------------------------------------------------------------------------
11 -- Module : Text.Shakespeare.I18N
12 -- Copyright : 2012 Michael Snoyman <michael@snoyman.com>, Jeremy Shaw
13 -- License : BSD-style (see the LICENSE file in the distribution)
15 -- Maintainer : Michael Snoyman <michael@snoyman.com>
16 -- Stability : experimental
17 -- Portability : portable
19 -- This module provides a type-based system for providing translations
22 -- It is similar in purpose to gettext or Java message bundles.
24 -- The core idea is to create simple data type where each constructor
25 -- represents a phrase, sentence, paragraph, etc. For example:
27 -- > data AppMessages = Hello | Goodbye
29 -- The 'RenderMessage' class is used to retrieve the appropriate
30 -- translation for a message value:
32 -- > class RenderMessage master message where
33 -- > renderMessage :: master -- ^ type that specifies which set of translations to use
34 -- > -> [Lang] -- ^ acceptable languages in descending order of preference
35 -- > -> message -- ^ message to translate
38 -- Defining the translation type and providing the 'RenderMessage'
39 -- instance in Haskell is not very translator friendly. Instead,
40 -- translations are generally provided in external translations
41 -- files. Then the 'mkMessage' Template Haskell function is used to
42 -- read the external translation files and automatically create the
43 -- translation type and the @RenderMessage@ instance.
45 -- A full description of using this module to create translations for @Hamlet@ can be found here:
47 -- <http://www.yesodweb.com/book/internationalization>
49 -- A full description of using the module to create translations for @HSP@ can be found here:
51 -- <http://happstack.com/docs/crashcourse/Templates.html#hsp-i18n>
53 -- You can also adapt those instructions for use with other systems.
54 module Hcompta.CLI.Lib.Shakespeare.Leijen
64 import Language.Haskell.TH.Syntax
65 import Control.Applicative ((<$>))
66 import Control.Monad (filterM, forM)
67 import Data.Text (Text, pack, unpack)
68 import System.Directory
69 import Data.Maybe (catMaybes)
70 import Data.List (isSuffixOf, sortBy, foldl')
71 import qualified Data.Map as Map
72 import qualified Data.ByteString as S
73 import Data.Text.Encoding (decodeUtf8)
74 import Data.Char (isSpace, toLower, toUpper)
75 import Data.Ord (comparing)
76 import Text.Shakespeare.Base (Deref (..), Ident (..), parseHash, derefToExp)
77 import Text.ParserCombinators.Parsec (parse, many, eof, many1, noneOf, (<|>))
78 import Control.Arrow ((***))
79 import Data.Monoid (mempty, mappend)
80 import qualified Data.Text as T
81 import Data.String (IsString (fromString))
83 import qualified Hcompta.Lib.Leijen as W
85 -- | 'ToMessage' is used to convert the value inside #{ } to 'Text'
87 -- The primary purpose of this class is to allow the value in #{ } to
88 -- be a 'String' or 'Text' rather than forcing it to always be 'Text'.
89 class ToMessage a where
90 toMessage :: a -> W.Doc
91 instance ToMessage W.Doc where
93 instance ToMessage Text where
94 toMessage = W.strict_text
95 instance ToMessage String where
96 toMessage = W.strict_text . Data.Text.pack
97 instance ToMessage Int where
99 instance ToMessage Integer where
100 toMessage = W.integer
102 -- | the 'RenderMessage' is used to provide translations for a message types
104 -- The 'master' argument exists so that it is possible to provide more
105 -- than one set of translations for a 'message' type. This is useful
106 -- if a library provides a default set of translations, but the user
107 -- of the library wants to provide a different set of translations.
108 class RenderMessage master message where
109 renderMessage :: master -- ^ type that specifies which set of translations to use
110 -> [Lang] -- ^ acceptable languages in descending order of preference
111 -> message -- ^ message to translate
114 instance RenderMessage master W.Doc where
115 renderMessage _ _ = id
116 instance RenderMessage master Text where
117 renderMessage _ _ = W.strict_text
119 -- | an RFC1766 / ISO 639-1 language code (eg, @fr@, @en-GB@, etc).
122 message_prefix :: String
123 message_prefix = "I18N_"
125 -- |generate translations from translation files
127 -- This function will:
129 -- 1. look in the supplied subdirectory for files ending in @.msg@
131 -- 2. generate a type based on the constructors found
133 -- 3. create a 'RenderMessage' instance
135 mkMessage :: String -- ^ base name to use for translation type
136 -> FilePath -- ^ subdirectory which contains the translation files
137 -> Lang -- ^ default translation language
139 mkMessage dt folder lang =
140 mkMessageCommon True message_prefix "Message" dt dt folder lang
143 -- | create 'RenderMessage' instance for an existing data-type
144 mkMessageFor :: String -- ^ master translation data type
145 -> String -- ^ existing type to add translations for
146 -> FilePath -- ^ path to translation folder
147 -> Lang -- ^ default language
149 mkMessageFor master dt folder lang = mkMessageCommon False "" "" master dt folder lang
151 -- | create an additional set of translations for a type created by `mkMessage`
152 mkMessageVariant :: String -- ^ master translation data type
153 -> String -- ^ existing type to add translations for
154 -> FilePath -- ^ path to translation folder
155 -> Lang -- ^ default language
157 mkMessageVariant master dt folder lang = mkMessageCommon False message_prefix "Message" master dt folder lang
159 -- |used by 'mkMessage' and 'mkMessageFor' to generate a 'RenderMessage' and possibly a message data type
160 mkMessageCommon :: Bool -- ^ generate a new datatype from the constructors found in the .msg files
161 -> String -- ^ string to append to constructor names
162 -> String -- ^ string to append to datatype name
163 -> String -- ^ base name of master datatype
164 -> String -- ^ base name of translation datatype
165 -> FilePath -- ^ path to translation folder
166 -> Lang -- ^ default lang
168 mkMessageCommon genType prefix postfix master dt folder lang = do
169 files <- qRunIO $ getDirectoryContents folder
170 let files' = filter (`notElem` [".", ".."]) files
171 (_files', contents) <- qRunIO $ fmap (unzip . catMaybes) $ mapM (loadLang folder) files'
173 mapM_ qAddDependentFile $ concat _files'
175 let contents' = Map.toList $ Map.fromListWith (++) contents
177 case lookup lang contents' of
178 Nothing -> error $ "Did not find main language file: " ++ unpack lang
179 Just def -> toSDefs def
180 mapM_ (checkDef sdef) $ map snd contents'
181 let mname = mkName $ dt ++ postfix
182 c1 <- fmap concat $ mapM (toClauses prefix dt) contents'
183 c2 <- mapM (sToClause prefix dt) sdef
187 then ((DataD [] mname [] (map (toCon dt) sdef) []) :)
191 (ConT ''RenderMessage `AppT` (ConT $ mkName master) `AppT` ConT mname)
192 [ FunD (mkName "renderMessage") $ c1 ++ c2 ++ [c3]
196 toClauses :: String -> String -> (Lang, [Def]) -> Q [Clause]
197 toClauses prefix dt (lang, defs) =
202 (pat, bod) <- mkBody dt (prefix ++ constr def) (map fst $ vars def) (content def)
203 guard <- fmap NormalG [|$(return $ VarE a) == pack $(lift $ unpack lang)|]
205 [WildP, ConP (mkName ":") [VarP a, WildP], pat]
206 (GuardedB [(guard, bod)])
209 mkBody :: String -- ^ datatype
210 -> String -- ^ constructor
211 -> [String] -- ^ variable names
214 mkBody dt cs vs ct = do
216 let pat = RecP (mkName cs) (map (varName dt *** VarP) vp)
217 let ct' = map (fixVars vp) ct
218 pack' <- [|W.strict_text . Data.Text.pack|]
219 tomsg <- [|toMessage|]
220 let ct'' = map (toH pack' tomsg) ct'
222 let app a b = InfixE (Just a) mapp (Just b)
227 (x:xs) -> return $ foldl' app x xs
230 toH pack' _ (Raw s) = pack' `AppE` SigE (LitE (StringL s)) (ConT ''String)
231 toH _ tomsg (Var d) = tomsg `AppE` derefToExp [] d
233 let y = mkName $ '_' : x
235 fixVars vp (Var d) = Var $ fixDeref vp d
236 fixVars _ (Raw s) = Raw s
237 fixDeref vp (DerefIdent (Ident i)) = DerefIdent $ Ident $ fixIdent vp i
238 fixDeref vp (DerefBranch a b) = DerefBranch (fixDeref vp a) (fixDeref vp b)
245 sToClause :: String -> String -> SDef -> Q Clause
246 sToClause prefix dt sdef = do
247 (pat, bod) <- mkBody dt (prefix ++ sconstr sdef) (map fst $ svars sdef) (scontent sdef)
249 [WildP, ConP (mkName "[]") [], pat]
253 defClause :: Q Clause
258 rm <- [|renderMessage|]
260 [VarP a, ConP (mkName ":") [WildP, VarP c], VarP d]
261 (NormalB $ rm `AppE` VarE a `AppE` VarE c `AppE` VarE d)
264 toCon :: String -> SDef -> Con
265 toCon dt (SDef c vs _) =
266 RecC (mkName $ message_prefix ++ c) $ map go vs
268 go (n, t) = (varName dt n, NotStrict, ConT $ mkName t)
270 varName :: String -> String -> Name
272 mkName $ concat [lower a, "Message", upper y]
274 lower (x:xs) = toLower x : xs
276 upper (x:xs) = toUpper x : xs
279 checkDef :: [SDef] -> [Def] -> Q ()
281 go (sortBy (comparing sconstr) x) (sortBy (comparing constr) y)
284 go [] (b:_) = error $ "Extra message constructor: " ++ constr b
286 | sconstr a < constr b = go as (b:bs)
287 | sconstr a > constr b = error $ "Extra message constructor: " ++ constr b
289 go' (svars a) (vars b)
291 go' ((an, at):as) ((bn, mbt):bs)
292 | an /= bn = error "Mismatched variable names"
297 | at == bt -> go' as bs
298 | otherwise -> error "Mismatched variable types"
299 go' [] [] = return ()
300 go' _ _ = error "Mistmached variable count"
302 toSDefs :: [Def] -> Q [SDef]
303 toSDefs = mapM toSDef
305 toSDef :: Def -> Q SDef
307 vars' <- mapM go $ vars d
308 return $ SDef (constr d) vars' (content d)
310 go (a, Just b) = return (a, b)
311 go (a, Nothing) = error $ "Main language missing type for " ++ show (constr d, a)
315 , svars :: [(String, String)]
316 , scontent :: [Content]
321 , vars :: [(String, Maybe String)]
322 , content :: [Content]
325 (</>) :: FilePath -> FilePath -> FilePath
326 path </> file = path ++ '/' : file
328 loadLang :: FilePath -> FilePath -> IO (Maybe ([FilePath], (Lang, [Def])))
329 loadLang folder file = do
330 let file' = folder </> file
331 isFile <- doesFileExist file'
332 if isFile && ".msg" `isSuffixOf` file
334 let lang = pack $ reverse $ drop 4 $ reverse file
335 defs <- loadLangFile file'
336 return $ Just ([file'], (lang, defs))
338 isDir <- doesDirectoryExist file'
342 (files, defs) <- unzip <$> loadLangDir file'
343 return $ Just (files, (lang, concat defs))
347 loadLangDir :: FilePath -> IO [(FilePath, [Def])]
348 loadLangDir folder = do
349 paths <- map (folder </>) . filter (`notElem` [".", ".."]) <$> getDirectoryContents folder
350 files <- filterM doesFileExist paths
351 dirs <- filterM doesDirectoryExist paths
353 forM files $ \file -> do
354 if ".msg" `isSuffixOf` file
356 defs <- loadLangFile file
357 return $ Just (file, defs)
360 langDirs <- mapM loadLangDir dirs
361 return $ catMaybes langFiles ++ concat langDirs
363 loadLangFile :: FilePath -> IO [Def]
364 loadLangFile file = do
365 bs <- S.readFile file
366 let s = unpack $ decodeUtf8 bs
367 defs <- fmap catMaybes $ mapM (parseDef . T.unpack . T.strip . T.pack) $ lines s
370 parseDef :: String -> IO (Maybe Def)
371 parseDef "" = return Nothing
372 parseDef ('#':_) = return Nothing
376 content' <- fmap compress $ parseContent $ dropWhile isSpace end'
378 [] -> error $ "Missing constructor: " ++ s
379 (w:ws) -> return $ Just Def
381 , vars = map parseVar ws
384 _ -> error $ "Missing colon: " ++ s
386 (begin, end) = break (== ':') s
388 data Content = Var Deref | Raw String
390 compress :: [Content] -> [Content]
392 compress (Raw a:Raw b:rest) = compress $ Raw (a ++ b) : rest
393 compress (x:y) = x : compress y
395 parseContent :: String -> IO [Content]
397 either (error . show) return $ parse go s s
403 go' = (Raw `fmap` many1 (noneOf "#")) <|> (fmap (either Raw Var) parseHash)
405 parseVar :: String -> (String, Maybe String)
407 case break (== '@') s of
408 (x, '@':y) -> (x, Just y)
411 data SomeMessage master = forall msg. RenderMessage master msg => SomeMessage msg
413 instance IsString (SomeMessage master) where
414 fromString = SomeMessage . T.pack
416 instance master ~ master' => RenderMessage master (SomeMessage master') where
417 renderMessage a b (SomeMessage msg) = renderMessage a b msg