In many languages, to do translations, a team would pull in a library to handle some of the basic internationalization (i18n). In Elm tho, we have access to tools to do this on the language level without a library & instead can use a couple of functions & unions—lessening our dependencies.
So what’s so awesome about using union types? Unlike a Dict
(or a JavaScript object), we get type safety for the translations to verify that they exist & we’ve covered all the cases. It’ll be very easy to reason about the translations since everything is just a couple of case
statements. It will look a bit fluffy, but that’s elm-format
for you.
The accompanying source code for this post is in this Git repository. There’s also a demo—so that’s kinda neat.
So What Are the Pieces to the Translation Puzzle?
Langauage
Our Language
is a union of the languages the application supports.
type Language
= EnUk
| EnUs
| EsMx
Phrase
These are the basic building blocks & represent the phrases that the application will be translating.
type Phrase
= Greeting String
| Name
| TextColor String
| Color
| Red
| Blue
| Green
Translator
An alias for function from our phrase to a string to display.
type alias Translator =
Phrase -> String
translate
Part 1: A top-level function that can be partially applied with our language at the view
level that selects the appropriate translate function for the rest of the application.
translate : Language -> Translator
translate lang =
case lang of
EnUk ->
EnUk.translate
EnUs ->
EnUs.translate
EsMx ->
EsMx.translate
Part 2: Breaking down the top-level function into sub-level translations
This will make this easier to digest along with giving us a hand-off-able file for translators to do their thing.
translate : Phrase -> String
translate phrase =
case phrase of
Greeting name ->
"Yo, " ++ name ++ "!"
Name ->
"Name"
TextColor color ->
"The color of this text is " ++ color ++ "."
Color ->
"Color"
Red ->
"red"
Blue ->
"blue"
Green ->
"green"
So between these 4.5 concepts we can wield an entire way to translate across our application. In this example an I18n
folder with a structure looks like this:
├── Languages
│ ├── EnUk.elm
│ ├── EnUs.elm
│ └── EsMx.elm
├── I18n.elm
└── Phrases.elm
…where Phrases.elm
contains our Phrase
union, Languages/*.elm
contains just the translate
function for its corresponding language, & I18n.elm
contains the rest of the Translator
, Language
union, & the top-level translate
.
Hold Up: There’s Another Thing We’ll Need Though
And that is a way to get from a string sent to our app to the Language
union. I’ll be using this guy since it fits my needs of peeling out navigator.language
from the browser (i.e. a string like en-US
or es_MX
). We would traverse the array from navigator.languages
if support was better:
import Regex
import String
toLanguage : String -> Language
toLanguage lang =
let
-- will split our string on non-chars, take the first
-- 2 matches, & lowercase them
codeFinder : String -> List String
codeFinder =
Regex.split (Regex.AtMost 2) (Regex.regex "[^A-Za-z]")
>> List.take 2
>> List.map String.toLower
-- pattern that regex into Tuple2 of Maybes
-- containing the ( language code, country code )
locale : ( Maybe String, Maybe String )
locale =
case codeFinder lang of
a :: b :: _ ->
( Just a, Just b )
a :: _ ->
( Just a, Nothing )
_ ->
( Nothing, Nothing )
in
-- Using pattern matching & wildcards, we can
-- choose the appropriate language & fallbacks
case locale of
( Just "en", Just "uk" ) ->
EnUk
( Just "en", Just "au" ) ->
EnUk
( Just "en", Just "nz" ) ->
EnUk
( Just "en", _ ) ->
EnUs
( Just "es", _ ) ->
EsMx
_ ->
EnUs
So What’s the Most Basic Example of Putting This All Together?
import Html exposing (text)
import I18n.I18n as I18n exposing (Translator)
import I18n.Phrases as Phrases
main : Html String
main =
let
-- in an app, we’d build a `translate` function
-- here & hand it around to our views so they
-- can reference it
translate : Translator
translate =
I18n.translate (18n.toLanguage "en-US")
in
-- in composing `text` & `translate` we have
-- `Phrase -> Html String`
text (translate (Phrases.Greeting "toastal"))
-- displays: Yo, toastal!
But Let’s Get “Fancy” With an Example
Takeaway
Using a couple language-level features in Elm—unions, cases, & functions—we can create a seer stone for our app to translate all of our custom phrases. It’s hardly complicated enough to require a library for a simple use case. One thing to keep in mind tho is depending on your translation service, you may have to create some parser/Elm code generator to move from another format that’s not a .elm
file if required, but that shouldn’t be terribly complicated.