1{-# LANGUAGE TemplateHaskell #-}
2{-# LANGUAGE TypeFamilies #-}
3{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE TypeSynonymInstances #-}
6{-# LANGUAGE ExistentialQuantification #-}
7
8-----------------------------------------------------------------------------
9-- |
10-- Module      :  Text.Shakespeare.I18N
11-- Copyright   :  2012 Michael Snoyman <michael@snoyman.com>, Jeremy Shaw
12-- License     :  BSD-style (see the LICENSE file in the distribution)
13--
14-- Maintainer  :  Michael Snoyman <michael@snoyman.com>
15-- Stability   :  experimental
16-- Portability :  portable
17--
18-- This module provides a type-based system for providing translations
19-- for text strings.
20--
21-- It is similar in purpose to gettext or Java message bundles.
22--
23-- The core idea is to create simple data type where each constructor
24-- represents a phrase, sentence, paragraph, etc. For example:
25--
26-- > data AppMessages = Hello | Goodbye
27--
28-- The 'RenderMessage' class is used to retrieve the appropriate
29-- translation for a message value:
30--
31-- > class RenderMessage master message where
32-- >   renderMessage :: master  -- ^ type that specifies which set of translations to use
33-- >                 -> [Lang]  -- ^ acceptable languages in descending order of preference
34-- >                 -> message -- ^ message to translate
35-- >                 -> Text
36--
37-- Defining the translation type and providing the 'RenderMessage'
38-- instance in Haskell is not very translator friendly. Instead,
39-- translations are generally provided in external translations
40-- files. Then the 'mkMessage' Template Haskell function is used to
41-- read the external translation files and automatically create the
42-- translation type and the @RenderMessage@ instance.
43--
44-- A full description of using this module to create translations for @Hamlet@ can be found here:
45--
46--  <http://www.yesodweb.com/book/internationalization>
47--
48-- A full description of using the module to create translations for @HSP@ can be found here:
49--
50--  <http://happstack.com/docs/crashcourse/Templates.html#hsp-i18n>
51--
52-- You can also adapt those instructions for use with other systems.
53module Text.Shakespeare.I18N
54    ( mkMessage
55    , mkMessageFor
56    , mkMessageVariant
57    , RenderMessage (..)
58    , ToMessage (..)
59    , SomeMessage (..)
60    , Lang
61    ) where
62
63import Language.Haskell.TH.Syntax
64import Control.Applicative ((<$>))
65import Control.Monad (filterM, forM)
66import Data.Text (Text, pack, unpack)
67import System.Directory
68import Data.Maybe (catMaybes)
69import Data.List (isSuffixOf, sortBy, foldl')
70import qualified Data.Map as Map
71import qualified Data.ByteString as S
72import Data.Text.Encoding (decodeUtf8)
73import Data.Char (isSpace, toLower, toUpper)
74import Data.Ord (comparing)
75import Text.Shakespeare.Base (Deref (..), Ident (..), parseHash, derefToExp)
76import Text.ParserCombinators.Parsec (parse, many, eof, many1, noneOf, (<|>))
77import Control.Arrow ((***))
78import Data.Monoid (mempty, mappend)
79import qualified Data.Text as T
80import Data.String (IsString (fromString))
81
82-- | 'ToMessage' is used to convert the value inside #{ } to 'Text'
83--
84-- The primary purpose of this class is to allow the value in #{ } to
85-- be a 'String' or 'Text' rather than forcing it to always be 'Text'.
86class ToMessage a where
87    toMessage :: a -> Text
88instance ToMessage Text where
89    toMessage = id
90instance ToMessage String where
91    toMessage = Data.Text.pack
92
93-- | the 'RenderMessage' is used to provide translations for a message types
94--
95-- The 'master' argument exists so that it is possible to provide more
96-- than one set of translations for a 'message' type. This is useful
97-- if a library provides a default set of translations, but the user
98-- of the library wants to provide a different set of translations.
99class RenderMessage master message where
100    renderMessage :: master  -- ^ type that specifies which set of translations to use
101                  -> [Lang]  -- ^ acceptable languages in descending order of preference
102                  -> message -- ^ message to translate
103                  -> Text
104
105instance RenderMessage master Text where
106    renderMessage _ _ = id
107
108-- | an RFC1766 / ISO 639-1 language code (eg, @fr@, @en-GB@, etc).
109type Lang = Text
110
111-- |generate translations from translation files
112--
113-- This function will:
114--
115--  1. look in the supplied subdirectory for files ending in @.msg@
116--
117--  2. generate a type based on the constructors found
118--
119--  3. create a 'RenderMessage' instance
120--
121mkMessage :: String   -- ^ base name to use for translation type
122          -> FilePath -- ^ subdirectory which contains the translation files
123          -> Lang     -- ^ default translation language
124          -> Q [Dec]
125mkMessage dt folder lang =
126    mkMessageCommon True "Msg" "Message" dt dt folder lang
127
128
129-- | create 'RenderMessage' instance for an existing data-type
130mkMessageFor :: String     -- ^ master translation data type
131             -> String     -- ^ existing type to add translations for
132             -> FilePath   -- ^ path to translation folder
133             -> Lang       -- ^ default language
134             -> Q [Dec]
135mkMessageFor master dt folder lang = mkMessageCommon False "" "" master dt folder lang
136
137-- | create an additional set of translations for a type created by `mkMessage`
138mkMessageVariant :: String     -- ^ master translation data type
139                 -> String     -- ^ existing type to add translations for
140                 -> FilePath   -- ^ path to translation folder
141                 -> Lang       -- ^ default language
142                 -> Q [Dec]
143mkMessageVariant master dt folder lang = mkMessageCommon False "Msg" "Message" master dt folder lang
144
145-- |used by 'mkMessage' and 'mkMessageFor' to generate a 'RenderMessage' and possibly a message data type
146mkMessageCommon :: Bool      -- ^ generate a new datatype from the constructors found in the .msg files
147                -> String    -- ^ string to append to constructor names
148                -> String    -- ^ string to append to datatype name
149                -> String    -- ^ base name of master datatype
150                -> String    -- ^ base name of translation datatype
151                -> FilePath  -- ^ path to translation folder
152                -> Lang      -- ^ default lang
153                -> Q [Dec]
154mkMessageCommon genType prefix postfix master dt folder lang = do
155    files <- qRunIO $ getDirectoryContents folder
156    let files' = filter (`notElem` [".", ".."]) files
157    (filess, contents) <- qRunIO $ fmap (unzip . catMaybes) $ mapM (loadLang folder) files'
158    (mapM_.mapM_) addDependentFile filess
159    let contents' = Map.toList $ Map.fromListWith (++) contents
160    sdef <-
161        case lookup lang contents' of
162            Nothing -> error $ "Did not find main language file: " ++ unpack lang
163            Just def -> toSDefs def
164    mapM_ (checkDef sdef) $ map snd contents'
165    let mname = mkName $ dt ++ postfix
166    c1 <- fmap concat $ mapM (toClauses prefix dt) contents'
167    c2 <- mapM (sToClause prefix dt) sdef
168    c3 <- defClause
169    return $
170     ( if genType
171       then ((DataD [] mname [] Nothing (map (toCon dt) sdef) []) :)
172       else id)
173        [ instanceD
174            []
175            (ConT ''RenderMessage `AppT` (ConT $ mkName master) `AppT` ConT mname)
176            [ FunD (mkName "renderMessage") $ c1 ++ c2 ++ [c3]
177            ]
178        ]
179
180toClauses :: String -> String -> (Lang, [Def]) -> Q [Clause]
181toClauses prefix dt (lang, defs) =
182    mapM go defs
183  where
184    go def = do
185        a <- newName "lang"
186        (pat, bod) <- mkBody dt (prefix ++ constr def) (map fst $ vars def) (content def)
187        guard <- fmap NormalG [|$(return $ VarE a) == pack $(lift $ unpack lang)|]
188        return $ Clause
189            [WildP, ConP (mkName ":") [VarP a, WildP], pat]
190            (GuardedB [(guard, bod)])
191            []
192
193mkBody :: String -- ^ datatype
194       -> String -- ^ constructor
195       -> [String] -- ^ variable names
196       -> [Content]
197       -> Q (Pat, Exp)
198mkBody dt cs vs ct = do
199    vp <- mapM go vs
200    let pat = RecP (mkName cs) (map (varName dt *** VarP) vp)
201    let ct' = map (fixVars vp) ct
202    pack' <- [|Data.Text.pack|]
203    tomsg <- [|toMessage|]
204    let ct'' = map (toH pack' tomsg) ct'
205    mapp <- [|mappend|]
206    let app a b = InfixE (Just a) mapp (Just b)
207    e <-
208        case ct'' of
209            [] -> [|mempty|]
210            [x] -> return x
211            (x:xs) -> return $ foldl' app x xs
212    return (pat, e)
213  where
214    toH pack' _ (Raw s) = pack' `AppE` SigE (LitE (StringL s)) (ConT ''String)
215    toH _ tomsg (Var d) = tomsg `AppE` derefToExp [] d
216    go x = do
217        let y = mkName $ '_' : x
218        return (x, y)
219    fixVars vp (Var d) = Var $ fixDeref vp d
220    fixVars _ (Raw s) = Raw s
221    fixDeref vp (DerefIdent (Ident i)) = DerefIdent $ Ident $ fixIdent vp i
222    fixDeref vp (DerefBranch a b) = DerefBranch (fixDeref vp a) (fixDeref vp b)
223    fixDeref _ d = d
224    fixIdent vp i =
225        case lookup i vp of
226            Nothing -> i
227            Just y -> nameBase y
228
229sToClause :: String -> String -> SDef -> Q Clause
230sToClause prefix dt sdef = do
231    (pat, bod) <- mkBody dt (prefix ++ sconstr sdef) (map fst $ svars sdef) (scontent sdef)
232    return $ Clause
233        [WildP, ConP (mkName "[]") [], pat]
234        (NormalB bod)
235        []
236
237defClause :: Q Clause
238defClause = do
239    a <- newName "sub"
240    c <- newName "langs"
241    d <- newName "msg"
242    rm <- [|renderMessage|]
243    return $ Clause
244        [VarP a, ConP (mkName ":") [WildP, VarP c], VarP d]
245        (NormalB $ rm `AppE` VarE a `AppE` VarE c `AppE` VarE d)
246        []
247
248toCon :: String -> SDef -> Con
249toCon dt (SDef c vs _) =
250    RecC (mkName $ "Msg" ++ c) $ map go vs
251  where
252    go (n, t) = (varName dt n, notStrict, ConT $ mkName t)
253
254varName :: String -> String -> Name
255varName a y =
256    mkName $ concat [lower a, "Message", upper y]
257  where
258    lower (x:xs) = toLower x : xs
259    lower [] = []
260    upper (x:xs) = toUpper x : xs
261    upper [] = []
262
263checkDef :: [SDef] -> [Def] -> Q ()
264checkDef x y =
265    go (sortBy (comparing sconstr) x) (sortBy (comparing constr) y)
266  where
267    go _ [] = return ()
268    go [] (b:_) = error $ "Extra message constructor: " ++ constr b
269    go (a:as) (b:bs)
270        | sconstr a < constr b = go as (b:bs)
271        | sconstr a > constr b = error $ "Extra message constructor: " ++ constr b
272        | otherwise = do
273            go' (svars a) (vars b)
274            go as bs
275    go' ((an, at):as) ((bn, mbt):bs)
276        | an /= bn = error "Mismatched variable names"
277        | otherwise =
278            case mbt of
279                Nothing -> go' as bs
280                Just bt
281                    | at == bt -> go' as bs
282                    | otherwise -> error "Mismatched variable types"
283    go' [] [] = return ()
284    go' _ _ = error "Mistmached variable count"
285
286toSDefs :: [Def] -> Q [SDef]
287toSDefs = mapM toSDef
288
289toSDef :: Def -> Q SDef
290toSDef d = do
291    vars' <- mapM go $ vars d
292    return $ SDef (constr d) vars' (content d)
293  where
294    go (a, Just b) = return (a, b)
295    go (a, Nothing) = error $ "Main language missing type for " ++ show (constr d, a)
296
297data SDef = SDef
298    { sconstr :: String
299    , svars :: [(String, String)]
300    , scontent :: [Content]
301    }
302
303data Def = Def
304    { constr :: String
305    , vars :: [(String, Maybe String)]
306    , content :: [Content]
307    }
308
309(</>) :: FilePath -> FilePath -> FilePath
310path </> file = path ++ '/' : file
311
312loadLang :: FilePath -> FilePath -> IO (Maybe ([FilePath], (Lang, [Def])))
313loadLang folder file = do
314    let file' = folder </> file
315    isFile <- doesFileExist file'
316    if isFile && ".msg" `isSuffixOf` file
317        then do
318            let lang = pack $ reverse $ drop 4 $ reverse file
319            defs <- loadLangFile file'
320            return $ Just ([file'], (lang, defs))
321        else do
322            isDir <- doesDirectoryExist file'
323            if isDir
324                then do
325                    let lang = pack file
326                    (files, defs) <- unzip <$> loadLangDir file'
327                    return $ Just (files, (lang, concat defs))
328                else
329                    return Nothing
330
331loadLangDir :: FilePath -> IO [(FilePath, [Def])]
332loadLangDir folder = do
333    paths <- map (folder </>) . filter (`notElem` [".", ".."]) <$> getDirectoryContents folder
334    files <- filterM doesFileExist paths
335    dirs  <- filterM doesDirectoryExist paths
336    langFiles <-
337        forM files $ \file -> do
338            if ".msg" `isSuffixOf` file
339                then do
340                  defs <- loadLangFile file
341                  return $ Just (file, defs)
342                else do
343                  return Nothing
344    langDirs <- mapM loadLangDir dirs
345    return $ catMaybes langFiles ++ concat langDirs
346
347loadLangFile :: FilePath -> IO [Def]
348loadLangFile file = do
349    bs <- S.readFile file
350    let s = unpack $ decodeUtf8 bs
351    defs <- fmap catMaybes $ mapM (parseDef . T.unpack . T.strip . T.pack) $ lines s
352    return defs
353
354parseDef :: String -> IO (Maybe Def)
355parseDef "" = return Nothing
356parseDef ('#':_) = return Nothing
357parseDef s =
358    case end of
359        ':':end' -> do
360            content' <- fmap compress $ parseContent $ dropWhile isSpace end'
361            case words begin of
362                [] -> error $ "Missing constructor: " ++ s
363                (w:ws) -> return $ Just Def
364                            { constr = w
365                            , vars = map parseVar ws
366                            , content = content'
367                            }
368        _ -> error $ "Missing colon: " ++ s
369  where
370    (begin, end) = break (== ':') s
371
372data Content = Var Deref | Raw String
373
374compress :: [Content] -> [Content]
375compress [] = []
376compress (Raw a:Raw b:rest) = compress $ Raw (a ++ b) : rest
377compress (x:y) = x : compress y
378
379parseContent :: String -> IO [Content]
380parseContent s =
381    either (error . show) return $ parse go s s
382  where
383    go = do
384        x <- many go'
385        eof
386        return x
387    go' = (Raw `fmap` many1 (noneOf "#")) <|> (fmap (either Raw Var) parseHash)
388
389parseVar :: String -> (String, Maybe String)
390parseVar s =
391    case break (== '@') s of
392        (x, '@':y) -> (x, Just y)
393        _ -> (s, Nothing)
394
395data SomeMessage master = forall msg. RenderMessage master msg => SomeMessage msg
396
397instance IsString (SomeMessage master) where
398    fromString = SomeMessage . T.pack
399
400instance master ~ master' => RenderMessage master (SomeMessage master') where
401    renderMessage a b (SomeMessage msg) = renderMessage a b msg
402
403notStrict :: Bang
404notStrict = Bang NoSourceUnpackedness NoSourceStrictness
405
406instanceD :: Cxt -> Type -> [Dec] -> Dec
407instanceD = InstanceD Nothing
408