{-# LANGUAGE RecordWildCards, OverloadedStrings #-}
-- | This is the main module of @haskell-gettext@ package.
-- For most cases, it is enough to import only this module.
-- Other modules of the package might be useful for other libraries
-- working with gettext's files.
--
-- Simple example of usage of this module is:
--
-- @
-- {-\# LANGUAGE OverloadedStrings #\-}
-- module Main where
--
-- import qualified Data.Text.Lazy as T
-- import qualified Text.Lazy.IO as TLIO
-- import Text.Printf
--
-- import Data.Gettext
--
-- main :: IO ()
-- main = do
-- catalog <- loadCatalog "locale\/ru\/messages.mo"
-- TLIO.putStrLn $ gettext catalog "Simple translated message"
-- let n = 78
-- let template = ngettext catalog "There is %d file" "There are %d files" n
-- printf (T.unpack template) n
-- @
--
module Data.Gettext
( -- * Data structures
Catalog,
-- * Loading and using translations
loadCatalog,
lookup,
gettext, cgettext,
ngettext, cngettext,
ngettext',
assocs,
-- * Utilities for plural forms
getHeaders,
getPluralDefinition,
choosePluralForm,
-- * Utilities for custom parsers implementation
parseGmo,
unpackGmoFile
) where
import Prelude hiding (lookup)
import Data.Binary
import Data.Binary.Get
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as TLE
import qualified Data.Trie as Trie
import Text.Printf
import Data.Gettext.GmoFile
import Data.Gettext.Plural
import Data.Gettext.Parsers
-- | This structure describes data in Gettext's @.mo/.gmo@ file in ready-to-use format.
data Catalog = Catalog {
gmoSize :: Word32,
gmoChoosePlural :: Int -> Int,
gmoData :: Trie.Trie [T.Text] }
instance Show Catalog where
show gmo = printf "<GetText data size=%d>" (gmoSize gmo)
-- | Load gettext file
loadCatalog :: FilePath -> IO Catalog
loadCatalog path = do
content <- L.readFile path
let gmoFile = (runGet parseGmo content) {fData = content}
return $ unpackGmoFile gmoFile
-- | Look up for string translation
lookup :: B.ByteString -> Catalog -> Maybe [T.Text]
lookup key gmo = Trie.lookup key (gmoData gmo)
-- | Get all translation pairs
assocs :: Catalog -> [(B.ByteString, [T.Text])]
assocs = Trie.toList . gmoData
-- | Obtain headers of the catalog.
-- Headers are defined as a translation for empty string.
getHeaders :: Catalog -> Maybe Headers
getHeaders gmo = getHeaders' (gmoData gmo)
getHeaders' :: Trie.Trie [T.Text] -> Maybe Headers
getHeaders' trie =
case Trie.lookup "" trie of
Nothing -> Nothing
Just texts -> either error Just $ parseHeaders (head texts)
-- | Get plural forms selection definition.
getPluralDefinition :: Catalog -> Maybe (Int, Expr)
getPluralDefinition gmo = getPluralDefinition' (gmoData gmo)
getPluralDefinition' :: Trie.Trie [T.Text] -> Maybe (Int, Expr)
getPluralDefinition' trie =
case getHeaders' trie of
Nothing -> Nothing
Just headers -> either error Just $ parsePlural headers
-- | Translate a string.
-- Original message must be defined in @po@ file in @msgid@ line.
gettext :: Catalog
-> B.ByteString -- ^ Original string
-> T.Text
gettext gmo key =
case lookup key gmo of
Nothing -> TLE.decodeUtf8 $ L.fromStrict key
Just texts -> head texts
-- | Translate a string within specific context.
cgettext :: Catalog
-> B.ByteString -- ^ Message context (@msgctxt@ line in @po@ file)
-> B.ByteString -- ^ Original string
-> T.Text
cgettext gmo context key = gettext gmo (context `B.append` "\4" `B.append` key)
-- | Translate a string and select correct plural form.
-- Original single form must be defined in @po@ file in @msgid@ line.
-- Original plural form must be defined in @po@ file in @msgid_plural@ line.
ngettext :: Catalog
-> B.ByteString -- ^ Single form in original language
-> B.ByteString -- ^ Plural form in original language
-> Int -- ^ Number
-> T.Text
ngettext gmo single plural n = ngettext' gmo (single `B.append` "\0" `B.append` plural) n
-- | Translate a string and select correct plural form, within specific context
-- Original single form must be defined in @po@ file in @msgid@ line.
-- Original plural form must be defined in @po@ file in @msgid_plural@ line.
cngettext :: Catalog
-> B.ByteString -- ^ Message context (@msgctxt@ line in @po@ file)
-> B.ByteString -- ^ Single form in original language
-> B.ByteString -- ^ Plural form in original language
-> Int -- ^ Number
-> T.Text
cngettext gmo context single plural n =
ngettext' gmo (context `B.append` "\4" `B.append` single `B.append` "\0" `B.append` plural) n
-- | Variant of @ngettext@ for case when for some reason there is only
-- @msgid@ defined in @po@ file, and no @msgid_plural@, but there are some @msgstr[n]@.
ngettext' :: Catalog
-> B.ByteString -- ^ Single form in original language
-> Int -- ^ Number
-> T.Text
ngettext' gmo key n =
case lookup key gmo of
Nothing -> TLE.decodeUtf8 $ L.fromStrict key
Just texts ->
let plural = choosePluralForm gmo n
idx = if plural >= length texts
then 0 -- if there are not enough plural forms defined, always use the first,
-- it is probably the most correct
else plural
in texts !! idx
-- | Choose plural form index by number
choosePluralForm :: Catalog -> Int -> Int
choosePluralForm gmo = gmoChoosePlural gmo
choosePluralForm' :: Trie.Trie [T.Text] -> Int -> Int
choosePluralForm' trie n =
case getPluralDefinition' trie of
Nothing -> if n == 1 then 0 else 1 -- from GNU gettext implementation, known as 'germanic plural form'
Just (_, expr) -> eval expr n
-- | Prepare the data parsed from file for lookups.
unpackGmoFile :: GmoFile -> Catalog
unpackGmoFile (GmoFile {..}) = Catalog fSize choose trie
where
getOrig (len,offs) = L.take (fromIntegral len) $ L.drop (fromIntegral offs) fData
choose = choosePluralForm' trie
getTrans (len,offs) =
let bstr = getOrig (len,offs)
in if L.null bstr
then [T.empty]
else map TLE.decodeUtf8 $ L.split 0 bstr
originals = map L.toStrict $ map getOrig fOriginals
translations = map getTrans fTranslations
trie = Trie.fromList $ zip originals translations