ihp icon indicating copy to clipboard operation
ihp copied to clipboard

I18n

Open mpscholten opened this issue 4 years ago • 11 comments

mpscholten avatar Mar 12 '21 08:03 mpscholten

haskell-gettext is a pure-Haskell library to parse gettext binary catalog (.mo / .gmo) files and provides functions like gettext and ngettext. I've got it running in a toy IHP example (using unsafePerformIO for now but that's just because I'm lazy).

The "usual" gettext workflow is to use xgettext to extract the to-be-translated strings from the source files. This doesn't support Haskell unfortunately. There's an alternative called hgettext which can parse Haskell syntax and create the .pot files, though it fails to work with files where HSX is present.

We should probably copy over / fork this tool from https://github.com/haskell-hvr/hgettext/blob/master/src-exe/hgettext.hs and make it work with IHP and HSX.

hendi avatar May 12 '21 14:05 hendi

@hendi is your toy example code online anywhere?

unhammer avatar Aug 26 '22 12:08 unhammer

Related to i18n in routing: https://github.com/digitallyinduced/ihp/pull/1486

Here's some real world code of how we build a i18n in an IHP app:


{-# LANGUAGE DeriveGeneric #-}
module Application.Localization
    ( Localizable(..)
    , Locale(..)
    , l10n
    , l10nText
    , localeFromHeader
    , defaultLocale
    , htmlLang
    , LLocation(..)
    , localizedPath
    , localizedPathTo
    , localizedRedirectTo
    ) where

import IHP.ViewPrelude
import Generated.Types
import IHP.ControllerPrelude
import qualified Data.Attoparsec.Text as A
import Control.Applicative (optional)
import Data.Char (isAlpha, isDigit)
import Data.Serialize (Serialize)
import GHC.Generics (Generic)

data Locale
    = DE_DE
    | EN_UK
    deriving (Eq, Show, Generic)

instance Serialize Locale

type AcceptLanguage = [(Text, [Text], Double)]

data Localizable
    = LFindYourDreamProperty
    | LApartment
    | LHouse
    | LSwitchToLanguage
    | LFrequentlyAskedQuestions




defaultLocale :: Locale
defaultLocale = DE_DE

currentLocale :: _ => Locale
currentLocale = fromMaybe defaultLocale $ maybeFromFrozenContext @Locale

l10nText :: _ => Localizable -> Text
l10nText l = l10nText' l currentLocale


-- In the view you can then do e.g. this:
[hsx|
    {(textField #firstname) { fieldLabel = l10nText LFirstName <> ":"}}
|]

mpscholten avatar Aug 26 '22 13:08 mpscholten

TODO: Add example to documentation.

s0kil avatar Aug 29 '22 18:08 s0kil

Thanks for this example. I got basic i18n working thanks to it. Do you have an example of how you handle setting the locale automatically from the HTTP header? I presume that is what localeFromHeader is for.

stephenbenedict avatar Feb 21 '23 11:02 stephenbenedict

How would you go about using currentLocale within a controller? I have separate database records for each language and so I would like to query the database records appropriate for the current locale before rendering the view. Trying to use currentLocale within a controller results in the error:

maybeFromFrozenContext called on a non frozen context while trying to access Locale

which I assume means that the locale is not set until a view is loaded. My FrontController is set up like in #1486

stephenbenedict avatar Mar 13 '24 11:03 stephenbenedict

Inside the controller you can use fromContext instead of maybeFromFrozenContext to access the locale

mpscholten avatar Mar 18 '24 08:03 mpscholten

Thank you, that indeed was the solution. However, now I'm struggling to figure out how to build a language toggle link. Do you have examples of how to do that? Specifically, I know how to prepend the locale (such as 'en') to the page path but I'm not sure how I would include a link to remove it to reverse the process.

stephenbenedict avatar Mar 26 '24 06:03 stephenbenedict

Here's some example code:

footer :: Html
footer = [hsx|
<footer class="site-footer-container container mt-auto">
    <div class="site-footer mt-4 py-4">
        <a href={switchLanguageHref}>{l10n LSwitchToLanguage}</a>
    </div>
</footer>
|]
    where
        req = request $ requestContext ?context
        currentPath = rawPathInfo req <> rawQueryString req
        switchLanguageHref = if B.isPrefixOf "/en" currentPath
            then case B.drop 3 currentPath of
                "" -> "/"
                p -> p
            else "/en" <> currentPath

mpscholten avatar Mar 26 '24 06:03 mpscholten

Thanks so much!

stephenbenedict avatar Mar 26 '24 09:03 stephenbenedict

@stephenbenedict Care to add it as a recipe on the docs? :pray:

amitaibu avatar Mar 26 '24 12:03 amitaibu