How to Internationalize & Translate Your MVP
by Denis Augsburger
When you develop a new software, web application or app you are confronted with the question of whether you should internationalize, localize and translate your application at the beginning or later on when you actually want to go global.
The internationalization (i18n), localization (l10n) and translation of a Minimum Viable Product (MVP) is a controversial topic. Meaning, there’s two sets of opinions when it comes to the point in time at which you should integrate i18n into your MVP:
- Wait, because your software should be minimal and without any unnecessary overhead (time-to-market, cost…)
- Do it right away, because it’s extremely time-consuming to implement i18n later on.
From my experience, implementing internationalization later on is likely going to triple your effort in comparison to the implementation of i18n in the beginning of your project. Therefore, I would personally recommend to generally implement i18n right from the start. There’s different ways to do that.
I18n Implementation of Your MVP
First, some general insights into the topic of internationalization (i18n) and localization (l10n) of your software. I’m going to look at questions like: What are the differences between i18n-libraries? What features exist and which ones do you need? What is necessary to translate locales efficiently?
Different Libraries & Features
For most programming languages there are multiple options to implement i18n. Your choice mainly depends on your requirements. In general, there are some more light-weighted i18n libraries - these are a good fit for smaller projects without the necessity to translate to a bright variety of languages - and some more advanced i18n libraries with more features supported. Let's look at what you can expect from an i18n library.
Use Interpolation to Add Data to Translation
Most i18n libraries do come with interpolation support, since it’s one of the most common features. It enables you to integrate dynamic values into your translation. This basically means you can insert data into your translations, for example your user's name. These interpolation-variables are specifically marked in your translations, so they can be rearranged to another place in the sentence if needed. If you don't use a custom mark, some default ones for different i18n libraries are in the list below:
Default for | Interpolation |
---|---|
polyglot.js, Ruby on Rails | %{ variable } |
Vue i18n, Format.js, Yii2, C#/.net | { variable } |
ngx-translate, Transloco, i18next | {{ variable }} |
java properties | ${ variable } |
Since grammar can vary widely, as a developer it is a good advice to use interpolation and not concatenate dynamic variables yourself in the application.
Some i18n libraries support the escaping of dynamic values. This is an important security feature when dealing with user-generated or dynamic content because it could lead to xss. To mitigate this issue it is therefore specifically handled by some i18n libraries.
Let's continue with an example to use the interpolation of Vue i18n and translate an error message of an input field that exceeds the maximum length. Instead of translating every field individually we provide the fieldname as well as the maximum length that the field can have.
// en.json
{
"link": "link",
"tooLong": "The maximum length of {fieldname} is {maxLength}"
}
// fr.json
{
"link": "lien",
"tooLong": "La longueur maximale du {fieldname} est de {maxLength}"
}
This implementation looks fine at first - it follows the DRY-principle and can be reused in multiple forms in our application. But two problems remain:
- The provided “fieldname” must be translated as well.
- The preposition "of" is translated differently i.e. in French depending on the provided fieldname.
To solve the first problem mentioned above you need to call the translation function two times, which is tedious. In our example, it looks like this:
<p>{{ $t("tooLong", {"fieldname": $t("link"), "maxLength": 10}) }}</p>
Here, we used the translation function twice, once to translate the error message and once to translate the fieldname. This can be simplified with the use of nesting (also called nested translation).
Add Nesting to Reference Existing Translation
Nested translations are references to another field. Instead of writing an interpolation variable - which is more dynamic - you can reference a translated field. The advantage is that you don't need to provide it dynamically and you only need to change the translation/the word in one place. Instead of calling the translation function multiple times, the nested translation is directly included. The interpolation example from above combined with nesting could look like this:
// en.json
{
"link": "link",
"tooLongLink": "The maximum length of $t(link) is {maxLength}"
}
// fr.json
{
"link": "lien",
"tooLongLink": "La longueur maximale du $t(link) est de {maxLength}"
}
Nesting can be useful, but it also leads to more separated and less readable translations. Some good use cases for nesting are:
- save/cancel labels (very repetitive)
- label of fields (Which can be changed more easily)
Using it when dealing with the label of fields, nesting gives you the ability to change labels more easily. But again, keep downsides like separation and readability in mind.
Use Correct Pluralization
Pluralization (or Plurals) within i18n means that one word is translated differently depending on the provided plural (or singular) number. The translation of the count categories zero, one, two, a few or many can be different depending on the language. If your i18n library supports pluralization it should handle these cases when you translate your software to more languages. Especially if you need to translate to Slavic languages, I recommend to always double check if the library support for pluralization is suitable, because the pluralization rules are more complicated than in other languages. In the following comparison of the word “banana” in English and Russian you see the different translation of pluralization:
Count | Category | English | Russian |
---|---|---|---|
0 | zero | no banana | нет бананов |
1, 21, 31 | one | one banana, {n} bananas | {n} банан |
2, 22, 23, 34 | few | {n} bananas | {n} банана |
26, 39, 48 | many | {n} bananas | {n} бананов |
Depending on the ending of the number you will translate something (i.e. banana) differently in Russian. So be prepared for these cases if you want to target Slavic languages.
Another special case is the determination of what “a few” or “many” of something are. The same plural number for a few eggs can already be “many” for cars. Therefore some i18n libraries let you define those ranges appropriately for different languages, sometimes called “interval pluralization”.
Built-in vs External Support to Format Numbers, Dates & Currencies
Some i18n libraries have built-in support to handle number formats, dates and currencies. As an alternative, you are free to handle that topic with another library (like moment.js) or use the built-in support of Internationalization API. If your software is data-heavy I recommend to base this decision on your preferences. In general, libraries like moment.js bring more features than just translating (i.e. calculating) and have some i18n proven default values for formatting dates and times. But it also comes at the cost of longer loading time, some people would even call them bloated.
Use ICU-Messageformat to Deal With More Complex Translations
ICU is an acronym of International Components for Unicode, a standard which is supported by different programming languages as well as some i18n libraries. You will need ICU for large projects with a lot of locales as well as more complex combinations, for example female/male combined with plurals:
The syntax for ICU-Messageformat is
// KIND = plural, select (fixed sub-message), selectordinal
"{KEY, KIND, options, \
// depending on the kind, for example select \
male { He has subscribed } \
female { She has subscribed } \
nonbinary { Hir has subscribed } \
other { Robot has subscribed } \
}"
The expected values for ICU-messages are
Kind | Values | Note |
---|---|---|
plural | zero, one, two, few, many, other | |
selectordinal | zero, one, two, few, many, other | ordinal ending - 1st, 2nd 3rd |
select | Any values (i.e. female, male, nonbinary), other |
The plural rules depend on the language, so one and two (category one&two depending on the language) is not the same as =1 or =2 (exactly 1 or 2). You can check out the CLDR Language Plural Rules to look up which category exists per language and what they define.
It is recommended to place complex arguments as the outermost structure of a message and write full sentences in their sub-messages. For the combination of select and plural arguments, place the fixed sets of choices (select) on the outside and nest the plural ones inside. Like this:
{
"invitationText": "{gender, select, \
female { \
{num_members, plural \
=1 {She invites you to collaborate with another teammate.} \
other {She invites you to collaborate with {num_members} teammates.} \
male { \
{num_members, plural \
=1 {He invites you to collaborate with another teammate.} \
other {He invites you to collaborate with {num_members} teammates} \
}",
"nameLabel": "name"
}
After internationalizing your MVP you have an i18n ready software and are probably eager to translate and see how it looks in other languages.
Translation of Your MVP
During development as well as before launching your MVP - or more general before going live in more languages for a bigger target audience - you want to translate it for better testing or evaluation of your MVP.
Since professional translations are expensive and time-consuming, depending on the progress and goal of your project, it might not be an option to use them, especially at the beginning. A less expensive alternative is to use machine translation (MT) services like Google Translate or DeepL. The quality of MT depends on the service as well as the language pair (source/target language) you want to translate. I generally consider DeepL to have better results but it doesn't support as many languages as Google Translate. Simpleen for example - a webtool to machine translate software, (Web)Apps and Games - currently uses DeepL to provide translations for i18n JSON, properties files and YAML locales.
From my experience, it is a good starting point to use machine translations for i18n software projects. As a result you can:
- test your MVP in another language with reasonable data
- stand out with i18n in presentations early on
- enter new markets
- collaborate with your users to improve translations
An MVP doesn't need to be perfect. But if you can fix problems earlier it will be much easier than later on. It also depends on the regions and your requirements which features you need. As a general rule, keep it as simple as possible and move to the more advanced features once you really need them.