Loco: Localization Package for Nim
There’re few i18n packages for Nim, all look unmaintained and undocumented. So I decided to make my own. Please welcome Loco.
Problem #
The goal is to have the usual i18n localization routine, where you declare language variables separately from the business logic, optionally with multiple variants for different quantities, and use them instead of plain text in your code.
How it’s done in other languages #
In other languages, language variables are usually invoked with a function call that accepts the name of the variable and the quantity to pluralize it with. In Python, for example, you’d write: i18n.t('mail_number', count=12)
.
Although this is classical approach most developers are used to, there’s one problem I see with it. Language variable names are just strings and strings are not validated. I you mistype a variable name, you either get a runtime error about a missing translation or, even worse, an empty string popping up in production.
How I do it in Loco #
Luckily, the issue described above is easily sovlable in Nim. Nim’s great metaprogramming capabilities let you generate code from declarative style definitions.
In Loco, you define your translations in .nim
files using the special loco
macro:
# Module with translations, i.e. `localizations/en.nim`
import loco
loco en:
hello: "hello"
Loco turns this declaration into a function definition:
proc hello*(): string = "hello"
This function can be imported and used in other modules like any other regular one:
# Other module containing the business logic
import localizations/en
echo hello()
To compile the app with a different localization, a compilation flag can be used:
const lang {.strdefine.}: string = "en"
when lang == "ru":
import localizations/ru
else:
import localizations/en
To switch between languages during runtime, import both localization modules and fully qualify the called functions:
echo "“Hello” in Russian is " & ru.hello()
echo "“Hello” in Englishis " & en.hello()
Let’s add a complex language variable to the localization module:
# Module with translations, i.e. `localizations/en.nim`
import loco
loco en:
hello: "hello"
users:
zero: "no users"
one: "one user"
many: "{n} users"
The generated users
proc will have an argument n: int
:
echo 0.users # → "zero users"
echo 1.users # → "one user"
echo 12.users # → "12 users"
The output of users
proc depends on the number passed to it. All cases are grouped into four tiers: zero, one, few, and many. The rules for matching numbers to tiers are defined in pluralizers. Pluralizer is a module within Loco. As of now, there are two: en
for English and ru
for Russian.
In the simplest case in English, only zero
and one
are special cases, all other numbers fall into many
tier, so the pluralizer is rather trivial (https://github.com/moigagoo/loco/blob/master/src/loco/en.nim):
template pluralize*(n: int): untyped =
## Pluralizer for English language.
case n
of 0: zero n
of 1: one n
else: many n
I think it’s a good idea to allow specific variants for specific values to have translation like “two users.” I may add this feature in future versions.
In Russian, the rules are trickier. You must take into account the number’s value and last digits. The Russian pluralizer is therefore more complicated and uses all four tiers.
How to add more languages to Loco #
Supporting a language means providing a pluralizer for it.
Adding a pluralizer is not hard. It’s a matter of adding a module in https://github.com/moigagoo/loco/tree/master/src/loco that implements exports pluralize(n: int)
. For any given n
, it must provide a branch of execution that ends with a call to zero
, one
, few
, or many
. It may use helper procs (like Russian pluralizer does) but only pluralize
must be exported.
Limitations #
This is my first approach to the task, so not everything I wanted to implement is currently implemented.
There are two most painful points:
- You can’t set language at runtime. If you have users logging to your site and each user has their own language preference, you msut render the site in this language. At the moment, there’s no easy way to do it since you have to use construction like this:
case user.lang
of en: renderPage(en.hello, en.account, en.signin)
of ru: renderPage(ru.hello, ru.account, ru.signin)
You can’t fall back to the default translation. If a language variable exists in one translation but not in the other, your app with compile with the first one and crash with the other. It may be usesul to know some translations are missing, but this should be at most a warning, not a compilation error. There must be a way to set a fallback language that is used when a translation is missing in the current one.
You can’t define translations in yaml, json, or ini. Not that Nim’s syntax is any worse, it’s just that yaml is more common.
Code review and contrinutions are very welcome #
I’m the only one writing in Nim among my colleagues and I’m not a professional software developer, so my code has to be poor. If you are a nimmer and have a spare minute, please review my code: https://github.com/moigagoo/loco.
The project is tiny and therefore easy to contrinute to. If you’re looking for a project to wet your feet with Nim, this is a good one. If you don’t know where to start, just email me (the address is in my GitHub profile) or ask right here in the comments.