In many languages, to do translations, a team would pull in a library to handle some of the basic internationalization (i18n). In Elm though, we have access to tools to do this on the language level without a library and instead can use a couple of functions and unions—lessening our dependencies.
So what’s so awesome about using union types? Unlike a
Dict, we get type safety for the translations to verify that they exist and 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.
So What Are the Pieces to the Translation Puzzle?
Language is a union of the languages the application supports.
type Language = EnUk | EnUs | EsMx
These are the basic building blocks and represent the phrases that the application will be translating.
type Phrase = Greeting String | Name | TextColor String | Color | Red | Blue | Green
An alias for function from our phrase to a string to display.
type alias Translator = Phrase -> String
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
Part2: Breaking down the top-level function into sub-level translations 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
Phrases.elm contains our
Languages/*.elm contains just the
translate function for its corresponding language, and
I18n.elm contains the rest of the
Language union, and the top-level
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
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, and lowercase them codeFinder : String -> List String codeFinder = List.map String.toLower << List.take 2 << Regex.split (Regex.AtMost 2) (Regex.regex "[^A-Za-z]") -- 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 and wildcards, we can -- choose the appropriate language and 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 and hand it around to our views so they -- can reference it translate : Translator translate = I18n.translate <| I18n.toLanguage "en-US" in -- in composing `text` and `translate` we have -- `Phrase -> Html String` text << translate <| Phrases.Greeting "toastal" -- displays: Yo, toastal!
But Let’s Get “Fancy” With an Example
(Those are air quotes)
Using a couple language-level features in Elm—unions, cases, and 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 though 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.