This library helps you to translate and localize your application. It is meant
to be used with kirchner/elm-cldr
,
which exposes all the localization data found in the Unicode Common Locale
Data Repository, and with
kirchner/elm-translation-runner
,
a command line tool for converting JSON files containing translations in the
ICU Message
Format
to Elm packages which expose these translations as data.
A very simple translation looks like this:
module Translations.En exposing (..)
import Text exposing (Text, Static, final, s)
greeting : Text Static args node
greeting =
s "Good morning!"
and to turn it into a String
you do
import Text exposing (asString)
import Translations.En exposing (..)
view =
greeting
|> asString
|> Html.text
You can also mix in placeholders into your translations:
module Translations.En exposing (..)
import Text exposing (Text, Static, final, s, string, concat)
question : Text Static { args | name : String, email : String } node
question =
concat
[ s "Hello, "
, string .name
, s "! Good to have you back. One question: is "
, string .email
, s " still your email address?"
]
You then have to provide values for these placeholders when using this translation:
import Text exposing (asStringWith)
import Translations.En exposing (..)
view name email =
question
|> asStringWith
{ name = name
, email = email
}
|> Html.text
Sometimes part of a translation have to be wrapped in another Html node, for example when the translation contains a link or parts of it should be bold. Instead of splitting the translation into parts or writing plain Html code in it, you can do the following:
import Html exposing (Html, a, div, strong, text)
import Html.Attributes exposing (href)
import Text exposing (Text, Static, final, s, concat, node, asNodes)
info :
Text
Static
{ args
| docsLink : List node -> node
, contactLink : List node -> node
, strong : List node -> node
}
node
info =
concat
[ s "You will find useful information in our "
, node .docsLink <|
s "documentation"
, s ". But if you have further questions, feel free to "
, node .contactLink <|
"contact us"
, s " at "
, node .strong <|
s "any time"
, s "!"
]
view : Html msg
view =
info
|> asNodes text
{ docsLink = a [ href ".." ]
, contactLink = a [ href " .." ]
, strong = strong []
}
|> div []
The view
function is then equivalent to:
view : Html msg
view =
div []
[ text "You will find useful information in our "
, a [ href ".." ] [ text "documentation" ]
, text ". But if you have further questions, feel free to "
, a [ href ".." ] [ text "contact us" ]
, text " at "
, strong [] [ text "any time" ]
, text "!"
]
Apart from simple string
placeholders, you can use placeholders for values of
type Float
, Dates
and Time
. For each of these placeholders, you either
have to provide a Printer a
, which encodes how the actual value is turned
into a String
:
module Translations.En exposing (..)
import Text exposing (Text Static, Printer, printer, final, s, float, concat)
mailboxInfo : Text Static { args | count : Float } node
mailboxInfo =
concat
[ s "Number of new mails: "
, float .count intPrinter
, s "."
]
intPrinter : Printer Float args node
intPrinter =
printer [ "int" ] <|
\float ->
float
|> floor
|> toString
|> s
Alternatively, you can use the Printer
's from
kirchner/elm-cldr
, which are
generated from the specifications in the CLDR and
cover a wide range of default formattings. A German translation of the previous
example using elm-cldr
would look like this:
module Translations.De exposing (..)
import Cldr.De exposing (decimalStandard)
import Text exposing (Text, Static, final, s, float, concat)
mailboxInfo : Text Static { args | count : Float } node
mailboxInfo =
concat
[ s "Anzahl neuer Emails: "
, float .count decimalStandard
, s "."
]
Note: Right now, kirchner/elm-cldr
does not expose Printer
's for
Date
's and Time
's, yet.
In a lot of languages translations change depending on a numerical value, for
example "1 second"
, "2 seconds"
, "1.0 seconds"
. The package
kirchner/elm-cldr
exposes helpers for
these situations. They let you do the following:
module Translations.De exposing (..)
import Cldr.De exposing (cardinal, decimalStandard)
import Text exposing (Text, Static, s, concat)
newMailsInfo : Text Static { args | count : Float } node
newMailsInfo =
cardinal .count
decimalStandard
[ ( 0, "Du hast keine neue Email." )
, ( 12, "Du hast ein Dutzend neue Emails." )
]
{ other =
concat
[ s "Du hast "
, count
, s " neue Emails."
]
, one =
concat
[ s "Du hast "
, count
, s " neue Email."
]
}
Then calling newMailsInfo |> asStringWith { count = 1 }
is equal to "Du hast 1 neue Email."
, whereas newMailsInfo |> asStringWith { count = 42 }
produces
"Du hast 42 neue Emails."
. There are helpers for cardinal and ordinal
pluralization rules of all locales defined in the
CLDR.
You can add quotation marks to your translations:
module Translations.De exposing (..)
import Text exposing (Text, Static, final, s, delimited, concat)
assumption : Text Static args node
assumption =
concat
[ s "Ihr fragt euch wahrscheinlich: "
, delimited quotePrinter <|
s "Was soll das alles?"
]
quotePrinter : Printer (Text m args node) args node
quotePrinter =
printer [ "quotes", "outer" ] <|
\text ->
concat
[ s "„"
, text
, s "“"
]
which will be printed as "Ihr fragt euch wahrscheinlich: „Was soll das alles?“"
. Also verbalizing lists is possible:
module Translations.En exposing (..)
import Text exposing (Text, Static, final, s, staticList, concat)
membersInfo : Text Static args node
membersInfo =
concat
[ s "These are all our members: "
, staticList andPrinter
[ s "Alice"
, s "Bob"
, s "Cindy"
]
]
andPrinter : Printer (List (Text args node)) args node
andPrinter =
printer [ "list", "and" ] <|
\texts ->
case List.reverse texts of
[] ->
s ""
lastText :: [] ->
lastText
lastText :: rest ->
[ [ lastText
, s " and "
]
, rest
|> List.intersperse (s ", ")
]
|> List.concat
|> List.reverse
|> concat
The printed result will be "These are all our members: Alice, Bob and Cindy"
.
Also, dynamic lists are possible:
module Translations.En exposing (..)
import Text exposing (Text, Static, final, s, list, concat)
fruitList : Text Static { args | fruits : List String } node
fruitList =
concat
[ s "Do you really want to by "
, list .fruits andPrinter
, s "?"
]
andPrinter : Printer (List String) args node
andPrinter =
printer [ "list", "and" ] <|
\strings ->
case List.reverse strings of
[] ->
s ""
lastString :: [] ->
s lastString
lastString :: rest ->
[ [ s lastString
, s " and "
]
, rest
|> List.intersperse (s ", ")
]
|> List.concat
|> List.reverse
|> concat
Note: kirchner/elm-cldr
will eventually also expose printers for
delimiters and lists.
Usually the actual translations for your application will not be created by
programmers but by translators. One therefore needs a way to generate
Translation
's of the above forms from --- for example --- strings in the ICU
Message
Format.
This can be done using the command line tool elm-translation
from
kirchner/elm-translation-runner
. It can also generate translation helpers
making it possible to switch between different languages in your application at
runtime.
If you have to be able to replace the translations in your application without a full deploy, we got you covered, as well! Suppose your translations module looks like this:
module Translations.En exposing (..)
import Text exposing (Text, Static, final, concat, s, string)
greeting : Text Static args node
greeting =
s "Good morning!"
question : Text Static { args | name : String, email : String } node
question =
concat
[ s "Hello, "
, string .name
, s "! Good to have you back. One question: is "
, string .email
, s " still your email address?"
]
You then can dynamically replace these translations like so:
import Html
import Text exposing (translateTo, asString, asStringWith)
import Translations.En exposing (..)
view name email =
Html.div []
[ greeting
|> name "greeting"
|> translateTo dynamicLocale
|> asString
|> Html.text
, question
|> name "question"
|> arg .name "name"
|> arg .email "email"
|> translateTo dynamicLocale
|> asStringWith
{ name = name
, email = email
}
|> Html.text
]
dynamicLocale : Locale args node
dynamicLocale =
Translation.locale
|> addTranslations
[ ( "greeting", "Guten morgen!" )
, ( "question"
, "Hallo, {name}! Schön, dass Du zurück bist."
++ " Eine Frage: ist {email} immer noch deine aktuelle Email-Adresse?"
)
]
In a real application you then probably want to load the list of translations
from a server and place dynamicLocale
inside your model. There are helpers to
add pluralization rules and printers to a Locale
.
Note the type change from Text Static args node
to Text Dynamic args node
:
greetingDynamic : Text Dynamic args node
greetingDynamic =
greeting
|> name "greeting"
|> translateTo dynamicLocale
questionDynamic : Text Dynamic args node
questionDynamic =
question
|> name "question"
|> arg .name "name"
|> arg .email "email"