What is a good way to internationalize an Elm application? - elm

I need to internationalize the UI strings in my ELM HTML application to 3 different languages.
I am thinking of doing this:
1) I will get the currentLanguage from Javascript, and pass it down in the ProgramWithFlags. I'llkeep the language in the model
2) I'll setup some types in my code
type alias Languages = English | French | Spanish
-- One of these for each string I want to internationalize
type alias InternationalizedStrings = StringHello | StringFoo | StringBar
3) I'll make a function for returning each translated phrase to use in my Views.
getPhrase: InternationalizationString Languages -> string
getPhrase stringId lang =
case lang of
English ->
case stringId of
StringHello -> "Hello"
StringFoo -> "Foo"
StringBar -> "Bar"
French ->
case stringId of
StringHello -> "Bonjour"
StringFoo -> "Oui"
StringBar -> "Non"
...
Is there a better way to do this? I have lots of string.

In case you want compiler errors when you don't provide the translation for a string your solution is on the right track.
If you want to either allow yet untranslated strings or find it tedious to have a type for every translatable string, you might want to switch to a Dict-based solution. to tinker with it, just throw it into http://elm-lang.org/try:
import Dict exposing (Dict)
import Html exposing (text)
type Language
= English
| French
| Spanish
type alias Key =
String
main =
text <| translate French "Hello"
translate : Language -> Key -> String
translate lang key =
let
dict =
case lang of
English ->
Dict.fromList
[ ( "Hello", "in english" )
]
French ->
Dict.fromList
[ ( "Hello", "salut" )
]
Spanish ->
Dict.fromList
[ ( "Hello", "hola" )
, ( "someKeyThatOnlyExistsInSpanish", "42" )
]
in
Dict.get key dict |> Maybe.withDefault ("can not find translation for " ++ key)

A while ago, I had a crack at internationalisation, and came up with the following setup:
define the language in a global model
have a very simple function, to be used in view modules and functions
the function has a signature of localString : Language -> String -> String
localString basically does a lookup in a global dictionary to find a translation from the word you provide to the language you provide.
it will always give back a String, defaulting to the original word, if it cannot find the word you provide, or if it cannot find the translation to the language you provide.
keep the global dictionary (and helper) functions NOT in the model, but in a separate file (it is pretty static data, which won't change in runtime).
the Language type is a Union Type, to ensure we only have 'approved' languages.
the actual dictionary uses conversions to string. The Dict type does not allow strong types as the key.
That way, using internationalisation has minimal impact on the rest of the code:
You need to add a Language to your Model (which you could get through JS port)
You can still use short and readable code in your views to translate, like
p [] [ text <| localString model.language "car" ]
All hardcoded strings in your own code remain in one simple default language, to keep the rest of your code readable.
Here is the gist of what I was working on, you can copy/ paste to elm-lang.org/try (not fully tested functionally or performance-wise with large numbers of strings and translations)
import Html exposing (div, p, text)
import Dict exposing (Dict)
-- Manage your languages below
type Language = English | Spanish | French
defaultLanguage : Language
defaultLanguage = English
languageToKey : Language -> LanguageKey
languageToKey language =
case language of
English -> "English"
Spanish -> "Spanish"
French -> "French"
keyToLanguage : LanguageKey -> Language
keyToLanguage key =
case key of
"English" -> English
"Spanish"-> Spanish
"French" -> French
_ -> defaultLanguage
english : LocalWord -> (Language, LocalWord)
english word =
(English, word)
spanish : LocalWord -> (Language, LocalWord)
spanish word =
(Spanish, word)
french : LocalWord -> (Language, LocalWord)
french word =
(French, word)
-- Internal stuff
type alias Word = String
type alias LocalWord = String
type alias LanguageKey = String
type alias Dictionary = Dict Word WordDict
type alias WordDict = Dict LanguageKey LocalWord
init : Dictionary
init =
Dict.fromList []
newLocalWord : Word -> (Language, LocalWord) -> Maybe WordDict -> Maybe WordDict
newLocalWord word (localLanguage, localWord) wordDict =
wordDict
|> Maybe.withDefault (Dict.fromList [])
|> Dict.insert (languageToKey defaultLanguage) word
|> Dict.insert (languageToKey localLanguage) localWord
|> Just
addTranslation : Word -> (Language, LocalWord) -> Dictionary -> Dictionary
addTranslation word newTranslation dictionary =
dictionary
|> Dict.update word (newLocalWord word newTranslation)
localString : Language -> Word -> LocalWord
localString language word =
let
wordEntry =
Dict.get word globalDictionary
localLanguage =
languageToKey language
in
case wordEntry of
Just wordDict ->
Dict.get localLanguage wordDict
|> Maybe.withDefault word
Nothing ->
word
add : Word -> List (Language, LocalWord) -> Dictionary -> Dictionary
add word translationList dictionary =
List.foldl (addTranslation word) dictionary translationList
-- BUILD DICTIONARY BELOW
globalDictionary : Dictionary
globalDictionary =
init
|> add "Hello" [ spanish "Hola", french "Bonjour" ]
|> add "Man" [ spanish "Hombre", french "Homme" ]
|> add "Child" [ french "Enfant" ]
-- For Elm-lang Try only
localModel =
{ language = Spanish }
main =
div []
[ p []
[ text <| "Hello in Spanish: "
++ localString localModel.language "Hello"
]
, p []
[ text <| "In dictionary, but not in Spanish: "
++ localString localModel.language "Child"
]
, p []
[ text <| "Is not in dictionary: "
++ localString localModel.language "Car"
]
]

I wrote a blog post about this a couple months ago. If you have the ability, try to prefer using ADTs over Dicts since Dicts can't give you the same guarantees at a type level (which is why Dict.get returns Maybe a). ADTs can also have the data type you're acting on type checked as well MyPhrase Int String that you can pattern match on and use whatever toString method you'd like (e.g. MyPhrase foo bar -> "My phrase contains " ++ toString foo ++ " & " ++ bar ++ "."). That being said, existing systems/translation services might make it difficult to use this method without writing a parser from .elm to .json or .po.

Related

How to test an Elm module without exposing everything?

Working my way through "Elm in Action", I understand that to write tests, all functions and types needed in a test suite for some module must be exposed by that module. This seems to break encapsulation. I don't want to expose internal functions and type constructors that should remain hidden just to make them testable. Is there a way to expose internal functions an types for testing only, but not for regular use?
There are a few strategies to tackle this problem, each with its pluses and minuses. As a running example, let's make a module that models a simple store on the server, that we would like to test the internals of:
module FooService exposing (Foo, all, update)
import Http
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
type Id
= Id String
type alias Foo =
{ id : Id
, title : String
}
apiBase : String
apiBase =
"https://example.com/api/v2"
all : (Result Http.Error (List Foo) -> msg) -> Cmd msg
all tagger =
Http.get
{ url = apiBase ++ "/foos"
, expect = Http.expectJson tagger decodeMany
}
update : Foo -> (Result Http.Error Foo -> msg) -> Cmd msg
update foo tagger =
Http.post
{ url = apiBase ++ "/foos/" ++ idToString foo.id
, body = foo |> encode |> Http.jsonBody
, expect = Http.expectJson tagger decode
}
idToString : Id -> String
idToString (Id id_) =
id_
encode : Foo -> Value
encode foo =
Encode.object
[ ( "id", Encode.string (idToString foo.id) )
, ( "title", Encode.string foo.title )
]
decode : Decoder Foo
decode =
Decode.map2 Foo
(Decode.field "id" (Decode.map Id Decode.string))
(Decode.field "title" Decode.string)
decodeMany : Decoder (List Foo)
decodeMany =
Decode.field "values" (Decode.list decode)
Note that as is, the module is ideally encapsulated, but utterly untestable. Let's look at some strategies to alleviate this problem:
1. Tests within modules
elm-test is not actually that prescriptive about where you put your tests, as long as there is an exposed value with the Test type.
As such you can do something like this:
module FooService exposing (Foo, all, update, testSuite)
-- FooService remains exactly the same, but the following is added
import Test
import Fuzz
testSuite : Test
testSuite =
Test.describe "FooService internals"
[ Test.fuzz (Fuzz.map2 Fuzz (Fuzz.map Id Fuzz.string) Fuzz.string) "Encoding roundtrips"
\foo ->
encode foo
|> Decode.decodeValue decoder
|> Expect.equal (Ok foo)
-- more tests here
]
This can be quite nice in the sense that tests are also collocated with the functions they are testing. The downside is that modules can get quite large with all the test code in them. It also requires you to move elm-test from your test dependencies into the runtime dependencies. This should theoretically not have any runtime impact, since elm's dead code elimination is excellent, but it does leave a lot of developers a little nervous.
2. Internal Modules
Another option, which is heavily used within elm packages (since there is direct support for that kind of hiding in the built in elm.json) is to have modules that are considered internal to a certain module or library and no other modules should read from them. This can be enforced by convention or I believe there are elm-review rules on can use to enforce these boundaries.
In our example it would look something like this:
module FooService.Internal exposing (Foo, Id(..), encode, decode, decodeMany, idToString)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
type Id
= Id String
type alias Foo =
{ id : Id
, title : String
}
idToString : Id -> String
idToString (Id id_) =
id_
encode : Foo -> Value
encode foo =
Encode.object
[ ( "id", Encode.string (idToString foo.id) )
, ( "title", Encode.string foo.title )
]
decode : Decoder Foo
decode =
Decode.map2 Foo
(Decode.field "id" (Decode.map Id Decode.string))
(Decode.field "title" Decode.string)
decodeMany : Decoder (List Foo)
decodeMany =
Decode.field "values" (Decode.list decode)
then FooService would simply become:
module FooService exposing (Foo, all, update)
import Http
import FooService.Internal as Internal
type alias Foo =
Internal.Foo
apiBase : String
apiBase =
"https://example.com/api/v2"
all : (Result Http.Error (List Foo) -> msg) -> Cmd msg
all tagger =
Http.get
{ url = apiBase ++ "/foos"
, expect = Http.expectJson tagger Internal.decodeMany
}
update : Foo -> (Result Http.Error Foo -> msg) -> Cmd msg
update foo tagger =
Http.post
{ url = apiBase ++ "/foos/" ++ Internal.idToString foo.id
, body = foo |> Internal.encode |> Http.jsonBody
, expect = Http.expectJson tagger Internal.decode
}
then all tests can be written against the internal module.
As I said, this is an extremely common pattern you will see in the majority of published elm packages, but in applications it suffers a bit from the fact that the tooling support isn't quite as good. For instance autocomplete will offer you these internal functions even in modules that shouldn't have access to them.
Nonetheless, we use this pattern quite successfully at work.
3. Change the design
Perhaps if a module isn't testable, then it is doing too much. One can look into things like the effect pattern to change the design to be more testable. For instance, one could argue that performing HTTP requests is outside the core competency of dealing with Foos, and the boundary should be at the decoder/encoder stage, which would make it quite testable; then a central module would deal with Http communication centrally.
We've been looking in this direction for a bit, but haven't found a good way to make it nice with really complex server interactions, but it might be something worth thinking about in each individual case: why is this module not testable? Would an alternative design be just as good and also be testable?

How to migrate from working input range to elm-mdl Slider?

I currently have a function that I use for a range, and number input type for each piece of data (trait). Updates to the model are done via EditTrait Mhid Relevance message.
valueRange : String -> TraitWithRelevance -> Html Msg
valueRange typ trait =
let
( name, mhid, relevance ) =
trait
in
input [ Attr.type_ typ, Attr.min "0", Attr.max "100", Attr.value relevance, Attr.step "1", onInput <| EditTrait mhid ] []
In an attempt to bring in Google's material design through elm-mdl, I want to replace the valueRange "range" call with the valueSlider function which utilizes the Slider component from elm-mdl.
The code below compiles, but obviously doesn't work, because the important onChange handler is missing. However, it renders correctly and the slider changes, when I update a trait's relevance value through the input number element.
valueSlider trait =
let
( name, mhid, relevance ) =
trait
relAsFloat =
String.toFloat relevance |> Result.toMaybe |> Maybe.withDefault 0
in
Slider.view
[ Slider.value relAsFloat
, Slider.min 0
, Slider.max 100
, Slider.step 1
]
When I throw in Slider.onChange (EditTrait mhid), which works on the regular input, the compiler gives me this error.
The argument to function onChange is causing a mismatch.
438| Slider.onChange (EditTrait mhid)
^^^^^^^^^^^^^^ Function onChange is expecting the argument to be:
Float -> m
But it is:
Relevance -> Msg
Detected errors in 1 module.
As onInput type is (String -> msg) -> Html.Attribute msg I suppose Relevance is a String, and EditTrait is mhid -> String -> Msg.
In this case, Slider.onChange (EditTrait mhid) doesn't work because Slider.onChange expects a Float -> Msg not Relevance -> Msg (as the compiler message reads.)
To solve this issue, you should change EditTrait to receive a Float instead of String. Changing Relevance type to be a Float and updating the code accordingly should do the trick.

Getting list element giving compilation error: "Arrows are reserved for cases and anonymous functions. Maybe you want > or >= instead?"

--define person type
type alias Person ={name: String, age: Int}
--created list of persons named people
people = [{name = "John", age = 41}, {name = "Nancy", age = 37}]
List item
names: List Person -> List String
names peeps = List.map(\peep -> peep.name) peeps
findPerson : String -> List Person -> Maybe Person
--using foldl to iterate list, here I am getting compile time error
findPerson name peeps = List.foldl(\peep memo -> case memo of Just _ ->
Nothing -> if peep.name == name then Just peep else Nothing ) Nothing peeps
main = text <| toString <| findPerson "John" people
Elm is whitespace-sensitive. The lines you flagged have invalid Elm code.
Your case statements should be in the form of:
case something of
Just val -> "we have a valid value: " ++ val
Nothing -> "we have nothing"
Furthermore, a foldl is probably not what you want for finding something in a list. You could get by with this implementation, which filters a list and takes the first element, if it exists.
findPerson : String -> List Person -> Maybe Person
findPerson name peeps =
List.filter (\peep -> peep.name == name) peeps
|> List.head

Does Elm's lambda syntax support destructuring?

Can I write
\(a, b) -> ...
and expect a tuple parameter to be destructured?
Yes, you can...
For example, following code works.
import Html exposing (text)
main =
("Hello", "World!")
|> \(a,b) -> a ++ " " ++ b
|> text

In Elm what is the correct way to implement my own toString

In Elm what is the correct way to take my Model and implement a toString function?
The type I am looking for would be toString : Model -> String, I am able to make a similar function with the type of toStr : Model -> String but I would think I would want the function to be called toString.
Example program (the Coin Changer kata):
module CoinChanger where
import Html exposing (..)
import StartApp.Simple as StartApp
import Signal exposing (Address)
import Html.Attributes exposing (..)
import Html.Events exposing (on, targetValue)
import String
---- MAIN ----
main =
StartApp.start
{
model = emptyModel
,update = update
,view = view
}
---- Model ----
type alias Model =
{
change : List Int
}
emptyModel : Model
emptyModel =
{
change = []
}
---- VIEW ----
toStr : Model -> String
toStr model =
model.change
|> List.map (\coin -> (toString coin) ++ "¢")
|> String.join ", "
view : Address String -> Model -> Html
view address model =
div []
[
input
[
placeholder "amount to make change for"
, on "input" targetValue (Signal.message address)
, autofocus True
-- style
]
[]
, div []
[
text (toStr model)
]
]
---- UPDATE ----
changeFor : Int -> List Int
changeFor amount =
[ 25, 10, 5, 1 ]
|> List.foldl
(\coin (change, amount)
-> ( change ++ List.repeat (amount // coin) coin
, amount % coin)
)
([], amount)
|> fst
update : String -> Model -> Model
update change model =
{ model | change =
case String.toInt change of
Ok amount
-> changeFor amount
Err msg
-> []
}
I would think the correct way to do this would be to call the function toString, but that gives me the following error from the compiler:
Detected errors in 1 module.
-- TYPE MISMATCH ----------------------------------------------- CoinChanger.elm
The type annotation for toString does not match its definition.
42│ toString : Model -> String
^^^^^^^^^^^^^^^ The type annotation is saying:
{ change : List Int } -> String
But I am inferring that the definition has this type:
{ change : List { change : List Int } } -> String
Renaming the function to toStr (or something not called toString) fixes the issue but seems wrong. What is the correct way to do this?
The problem is that, calling your function toString, you are overriding the toString function of the Basics module, which you are using at line 45.
To avoid this, you'll need to import the Basics module and use Basics.toString instead of simply toString to eliminare the ambiguity
The accepted answer is well out of date for anyone writing Elm 0.19+. The current solution is to write your own toString function for the type you want converted. There is a Debug.toString for use during development but its use in your code will prevent building for production.