i18n-0.4.0.0: src/Data/Text/I18n/Po.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module: Data.Text.I18n.Po
-- Copyright: (c) 2011-2016 Eugene Grigoriev
-- License: BSD3
-- Maintainer: Philip Cunningham <hello@filib.io>
-- Stability: experimental
-- Portability: portable
--
-- This module contains the PO parser. PO files are assumed to be in UTF-8
-- encoding. Plural forms are not yet implemented.
module Data.Text.I18n.Po (
-- * PO parsing
getL10n,
-- * I18n Monad Functions
localize,
gettext,
withContext,
withLocale,
-- * Parsing functions
parsePo,
) where
import Control.Applicative ((<$>))
import Control.Arrow (second, (&&&))
import Data.Either (partitionEithers)
import Data.Functor.Identity (Identity)
import Data.List (foldl', intercalate, isSuffixOf)
import qualified Data.Map as Map
import Data.Monoid (mconcat, mempty)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import System.Directory (getDirectoryContents)
import System.FilePath (pathSeparator)
import Text.Parsec
import Text.Parsec.Text
import Text.ParserCombinators.Parsec.Language
import qualified Text.ParserCombinators.Parsec.Token as P
import Data.Text.I18n
-- External functions
-- | Builds 'L10n' structure by parsing / .po / files contained in a given
-- directory. 'L10n' structure is to be passed to 'localize' function. 'L10n'
-- structure is used internaly by the 'I18n' monad.
getL10n :: FilePath
-- ^ Directory containing PO files.
-> IO (L10n, [ParseError])
-- ^ Localization structure and a list of parse errors.
getL10n dir = do
poFiles' <- poFiles dir
locs <- processPos (map (second parsePo) poFiles')
(es, locs') <- return $! partitionEithers locs
return (Map.fromList locs', es)
-- Internal Fuctions
processPos :: [(Locale, IO (Either ParseError [MsgDec]))]
-> IO [Either ParseError (Locale, CtxMap)]
processPos rs = do
rs' <- mapM (\(a, m) -> m >>= \b -> return (a, b)) rs
return $! map f rs'
where
f :: (a, Either b [MsgDec]) -> Either b (a, CtxMap)
f (l, Right msgdecs) = Right (l, mkMsgs msgdecs)
f (_, Left e) = Left e
mkMsgs :: [MsgDec] -> CtxMap
mkMsgs = foldl' f mempty
where
f :: CtxMap -> MsgDec -> CtxMap
f m (MsgDec ctxt msgid' msgstrs) =
case Map.lookup ctxt m of
Nothing -> f (Map.insert ctxt mempty m) (MsgDec ctxt msgid' msgstrs)
Just c -> Map.insert ctxt (Map.insert msgid' msgstrs c) m
-- | Finds all .po files for a given directory. Works on the assumption that the characters before
-- the file extension are the locale name.
poFiles :: FilePath -> IO [(Locale, FilePath)]
poFiles dir = do
files <- getDirectoryContents dir
return $! fmap assocLocalesAndPaths <$> onlyPoFiles $ files
where
toAbsolutePath :: FilePath -> FilePath
toAbsolutePath = intercalate [pathSeparator] . (dir :) . return
isPoFile :: FilePath -> Bool
isPoFile = flip any [".po", ".Po", ".PO"] . flip isSuffixOf
onlyPoFiles :: [FilePath] -> [FilePath]
onlyPoFiles = filter isPoFile
assocLocalesAndPaths :: FilePath -> (Locale, FilePath)
assocLocalesAndPaths = stripLocale &&& toAbsolutePath
stripLocale :: FilePath -> Locale
stripLocale path =
let n = subtract 3 . length $ path
in Locale . T.pack $! take n path
parsePo :: FilePath -> IO (Either ParseError [MsgDec])
parsePo path = do
contents <- T.readFile path
return $! parse po path contents
{- EBNF
PO ::= msg*
msg ::= [msg-context] (msg-singular | msg-plural)
msg-context ::= "msgctxt" string*
msg-singular ::= msgid msgstr
msg-plural ::= msgid msgid-plural msgstr-plural*
msgid ::= "msgid" string*
msgid-plural ::= "msgid_plural" string*
msgstr ::= "msgstr" string*
msgstr-plural ::= "msgstr" form string*
form ::= "[" number "]"
number ::= (0-9)* | "N"
string ::= "\"" (char | escaped-char)* "\""
escaped-char ::= "\\" char
char ::= (any UTF8 character)
-}
lexer :: P.GenTokenParser T.Text () Identity
lexer = P.makeTokenParser poLangDef
where
poLangDef :: GenLanguageDef T.Text st Identity
poLangDef = LanguageDef
{ commentStart = ""
, commentEnd = ""
, commentLine = "#"
, nestedComments = True
, identStart = letter <|> char '_'
, identLetter = alphaNum <|> oneOf "_'"
, opStart = opLetter poLangDef
, opLetter = oneOf ":!#$%&*+./<=>?@\\^|-~"
, reservedOpNames = []
, reservedNames = ["msgctxt", "msgid", "msgid_plural", "msgstr"]
, caseSensitive = True
}
whiteSpace :: Parser ()
whiteSpace = P.whiteSpace lexer
lexeme :: Parser a -> Parser a
lexeme = P.lexeme lexer
reserved :: String -> Parser ()
reserved = P.reserved lexer
po :: Parser [MsgDec]
po = do
_ <- whiteSpace
msgs <- many msg
_ <- eof
return $! msgs
msg :: Parser MsgDec
msg = do
ctxt <- msgContext
(id', strs) <- try msgSingular <|> msgPlural
return $! MsgDec ctxt id' strs
msgContext :: Parser (Maybe Context)
msgContext = try $ option Nothing $ do
_ <- lexeme (reserved "msgctxt")
strs <- many1 str
return (Just $! mconcat strs)
msgSingular :: Parser (Msgid, [Msgstr])
msgSingular = do
id' <- lexeme msgid
str' <- lexeme msgstr
return (Msgid id', [str'])
msgPlural :: Parser (Msgid, [Msgstr])
msgPlural = do
id' <- lexeme msgid
_ <- lexeme msgidPlural
strps <- lexeme (many1 msgstrPlural)
return (Msgid id', strps)
msgid :: Parser T.Text
msgid = do
_ <- lexeme (reserved "msgid")
strs <- many1 str
return $! mconcat strs
msgidPlural :: Parser Msgstr
msgidPlural = do
_ <- lexeme (reserved "msgid_plural")
strs <- many1 str
return $! mconcat strs
msgstr :: Parser Msgstr
msgstr = do
_ <- lexeme (reserved "msgstr")
strs <- many1 str
return $! mconcat strs
msgstrPlural :: Parser Msgstr
msgstrPlural = do
_ <- lexeme (reserved "msgstr")
_ <- char '['
_ <- try indice
_ <- char ']'
_ <- whiteSpace
strs <- many1 str
return $! mconcat strs
where
caseN :: Parser String
caseN = do
c <- oneOf ['n', 'N']
return [c]
caseX :: Parser String
caseX = many1 $ oneOf ['0' .. '9']
indice :: Parser String
indice = caseN <|> caseX
str :: Parser T.Text
str = lexeme $ do
_ <- char '"'
chs <- many char'
_ <- char '"'
return $! T.pack chs
char' :: Parser Char
char' = try escapedChar <|> noneOf ['"']
escapedChar :: Parser Char
escapedChar = do
e <- char '\\'
c <- anyChar
case reads ['\'', e, c, '\''] :: [(Char, String)] of
[(c', _)] -> return $! c'
_ -> return $! c