{-# LANGUAGE OverloadedStrings #-} module Snap.Snaplet.I18N ( I18N , HasI18N (..) , I18NMessage (..) , Locale , MessageFile , initI18N , getI18NMessages , lookupI18NValue ) where import Control.Monad.IO.Class import Control.Monad.State import qualified Data.Configurator as Config import qualified Data.Configurator.Types as Config import Data.Maybe import qualified Data.Text as T import Heist import qualified Heist.Interpreted as I import System.FilePath.Posix import Text.XmlHtml hiding (render) import qualified Text.XmlHtml as X import Paths_snaplet_i18n import Snap import Snap.Snaplet.Heist ---------------------------------------------------------------------- -- Types ---------------------------------------------------------------------- type Locale = String type MessageFile = String defaultLocale :: Locale defaultLocale = "en_US" -- | ?? could be multiple message files -- defaultMessageFilePrefix :: MessageFile defaultMessageFilePrefix = "message" data I18NConfig = I18NConfig { _getLocale :: Locale -- ^ locale, default "en" , _getMessageFile :: MessageFile -- ^ message file name, default to "message" } deriving (Show) -- | Message content. -- newtype I18NMessage = I18NMessage Config.Config -- | data type -- data I18N = I18N { _getI18NConfig :: I18NConfig , _getI18NMessage :: I18NMessage } -- | Compose App with a I18N Snaplet. -- class HasI18N b where i18nLens :: SnapletLens b I18N -- | Util functions -- getI18N :: HasI18N b => Handler b b I18N getI18N = with i18nLens get -- | Get the @I18NMessage@ -- getI18NMessages :: HasI18N b => Handler b b I18NMessage getI18NMessages = fmap _getI18NMessage getI18N -- | Look up a value in, usuallly Handler Monad -- lookupI18NValue :: HasI18N b => T.Text -> Handler b b T.Text lookupI18NValue key = do (I18NMessage msg) <- getI18NMessages liftIO $ Config.lookupDefault "Error: no value found." msg key ---------------------------------------------------------------------- -- Init Snaplet ---------------------------------------------------------------------- -- | Init this I18N snaplet. -- initI18N :: (HasHeist b, HasI18N b) => Maybe Locale -- ^ Locale, default to @defaultLocale@ -> SnapletInit b I18N initI18N l = makeSnaplet "i18n" description datadir $ do let i18nConfig = I18NConfig (fromMaybe defaultLocale l) defaultMessageFilePrefix fp <- getSnapletFilePath msg <- liftIO $ readMessageFile fp i18nConfig modifyHeistState addDefaultSplices return $ I18N i18nConfig msg where addDefaultSplices = I.bindSplices splices splices = do "i18n" ## i18nSplice "i18nSpan" ## i18nSpanSplice -- config dir for current snaplet datadir = Just $ fmap (++ "/resources") getDataDir description = "light weight i18n snaplet" -- | Load file -- server will not be able to start up if dir doesnt exists. -- Thus, no additional validation check so far. -- readMessageFile :: FilePath -> I18NConfig -> IO I18NMessage readMessageFile base config = do let fullname = base file config fmap I18NMessage (Config.load [Config.Required fullname]) where -- file fullname will be like message-en_US.cfg -- FIXME: Maybe replace "-" with "_" in locale in case typo file c = _getMessageFile c ++ "-" ++ _getLocale c ++ ".cfg" ---------------------------------------------------------------------- -- Splices ---------------------------------------------------------------------- -- | element attribute used for looking up i18n value. -- e.g. -- i18nSpliceAttr :: T.Text i18nSpliceAttr = "name" -- | Splices just wrap value fonud at l10n message. -- When it is used for wrap around other elements, a.k.a children is not empty, -- binding `i18nValue`. -- e.g. -- --

-- -- FIXME: Turns out that it is not possible to fail at compilation if value is Nothing but runtime. i18nSplice :: HasI18N b => I.Splice (Handler b b) i18nSplice = do input <- getParamNode value <- lift . lookupI18NValue $ getNameAttr input case childElements input of [] -> return [X.TextNode value] _ -> I.runChildrenWithText ("i18nValue" ## value) -- | Splices. use 'span' html element wrap result. -- i18nSpanSplice :: HasI18N b => I.Splice (Handler b b) i18nSpanSplice = do input <- getParamNode v <- lift . lookupI18NValue $ getNameAttr input return [X.Element "span" (elementAttrs input) [X.TextNode v]] -- | Look up 'name' attribute value. -- getNameAttr :: Node -> T.Text getNameAttr n = case getAttribute i18nSpliceAttr n of Just x -> x _ -> "" ----------------------------------------------------------------------