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