I was doing some research on i18n and discovered this rust crate, which got me thinking, how might this look in Roc.
I put this module together as a simple example, to see if people had any other ideas about how this might work?
https://gist.github.com/lukewilliamboswell/7cecc9cebf50436865752da96f4dada9
I could imagine in future this kind of module is code-generated from a list of text files that contain the translations required.
Using this in an app might look something like this...
main =
locale = Locale.get!? {} |> Result.map I18n.fromLocaleStr
Stdout.line (I18n.with Hello locale)
Unless you're translating the app yourself (which is unlikely), you probably want to pick a format that's supported by popular translation platforms.
I have experience using gettext and ICU, but Mozilla's Fluent looks pretty cool and modern too
Something I remember liking about gettext is that you can use the original language strings in the source code instead of having to define keys for everything
You just wrap translatable strings in a function, and then use xgettext to extract them from source and generate a template .pot file
At runtime, the strings are looked up in a binary .mo file that’s specific per locale. It’d probably be fine to load that entirely into memory so that string lookups don’t have to be effectful.
Ive thought about how we might be able to use compile time evaluation here, but haven't had any significant brainwaves -- getting the locale is a runtime thing
Yeah, I don’t think you want a binary per locale
I imagine the application has the translations in the (or one of the) binary encoded file(s) and then loads and decodes that and passes it in as a module param or something
and you probably don’t want to include all locales in the binary either, but that depends on the app
IIRC Linux (executable) binaries include the original language strings only, which are then used to look up the locale-specific string in the .mo file if available
I might be wrong though, it’s been a while since
For the 'location-string-as-a-function' approach Agus mentioned, an API like this might be nice to use:
# Translations.roc
module { locale } -> [hello, bye]
hello : { name : Str } -> Str
hello = \{ name } ->
when locale is
En -> "Hello, $(name)"
Fr -> "Bonjour, $(name)"
bye : Str
bye =
when locale is
En -> "Bye!"
Fr -> "Au revoir!"
Or maybe a record would be easier to dynamically load files
Translations : {
hello: { name : Str } -> Str,
bye: Str,
}
english = {
hello: \{ name } -> "Hello, $(name)",
bye: "Bye!",
}
french = {
hello: \{ name } -> "Bonjour, $(name)",
bye: "Au revoir!",
}
A combined approach:
# Translations.roc
module { locale } -> [hello, bye]
hello : { name : Str } -> Str
hello = locale.hello
bye : Str
bye = locale.bye
Here the Locale type would (secretly) contain the translations for that locale, and the Translations.roc module would just be a wrapper for nice access to translations.
A while ago I started work on some i18n in Roc using the CLDR data, it turns out there's a lot of locales and the Roc compiler at the time didn't like compiling a 4MB file :sweat_smile:
https://github.com/Hasnep/roc-cldr/blob/main/src/Date.roc
Last updated: Jun 16 2026 at 16:19 UTC