Coping with i18n makes it sound like it's some sort of autoimmune disease you have to learn how to live with. I18n (which stands for internationalization) is not a disease, but it is something you have to learn to live with. When we look up the meaning of "to cope with" in the dictionary, it says
to cope with: to deal with and attempt to overcome problems and difficulties
I18n does come with its challenges and difficulties. Especially in a project that started off small but is growing into something larger.
Fun fact: the 18 in i18n represents the 18 letters that stand between "i" and "n" in internationalization. So what would "accessibility" be?
Let's start with a use case:
A new project has started. It's not that big yet, but they do want translations for it. When adding internationalization, you just start using translation keys as you see fit. You have a label on one page that needs to be translated to "First name". You add a translation key to your translation file for this label:
{
"firstName": "First name"
}
Then, you have to create another page that also has a label that needs to be translated to "First name". Great! You can reuse the same translation key.
But the UX designer decided that on the first page "First name" is okay, but on the other one, it should be "What is your first name?".
No problem; you can just add a new key.
{
"firstName": "First name",
"whatIsYourFirstName": "What is your first name?"
}
But then a new change in translation comes up... It should no longer be "What is your first name?" but "Your first name please?". Time to update to translation then?
{
"firstName": "First name",
"whatIsYourFirstName": "Your first name please?"
}
That would be really confusing. Can you imagine a whole translation file with such keys?
Let's go over a few guidelines for i18n so it's easier to cope with it in your project.
A clean home is a happy home
Make sure that your translation file is up-to-date. Remove any unused translation keys and make sure all translation keys used in code are actually included in your translation file.
Some libraries support ways to handle missing translations (e.g., TranslocoMissingHandler from Transloco or MissingTranslationHandler
from ngx-translate). So that's a way to find out any missing translations, but that's quite manual because you have to go over every part of the served application.
Another way is to extract the translations from your template. For Angular i18n, you can use the extract-i18n Angular CLI command to extract the marked text in the component into a source language file. For ngx-translate, you can use the plugin ngx-translate-extract. And for transloco, there is the transloco-keys-manager.
It's important to keep the translation file up-to-date. Any keys that are used there that are not in the template may cause confusion. Any key missing in the translation file that is used in the template will hopefully result in an untranslated label (in the worst case, it will throw an error).
Be consistent
Some libraries provide different ways to translate stuff in your application. Some of the possibilities are functions, pipes, or directives. Be consistent, and keep using the same ones.
For an Angular project, for example, use directives in the template for translations. Or pipes. Just don't start mixing, because that would be ... inconsistent.
Another thing to be consistent about is the translation keys. Choose a certain case (camelCase, PascalCase, etc.) from the beginning and stick with it. If you happen to work with people who are not that proficient in English, it might be worth going all lowercase. One of the benefits of using all lowercase is that there can be no mistake when a word is made up of several words. For example, lowercase "deadline" is easy for everyone to write. But when using camelCase, some people might think deadline is made up of two words, so they come up with "deadLine". Then some other team member wants to use that translation key and uses "deadline" in the template. The result is that the translation is not showing up.
Conventions
Introduce conventions when working with translation files. The casing is one thing I already mentioned. Here are some others:
Avoid abbreviations
Don't use abbreviations in your translation keys. Most of the time, not everyone is aware of what the abbreviation means. A good example would be when you see the abbreviation "CI". As someone in development, you would think this stands for "Continuous Integration". But you were wrong; the application they used this abbreviation for was for the police department and stands for "Confidential Informant". So use "language" instead of "lang", use "component" instead of "comp", etc.
Use unambiguous names
Unambiguous names have a clear intent and are easy to understand. When choosing a translation key, keep that in mind. E.g., instead of "address1", "address2", use "homeAddress" and "invoiceAddress".
Use namespacing
Namespacing your translation keys is a good way to group them into categories. Depending on the translation library used, you could use nesting (like an object structure) or you can use namespacing in the key name (separated with a dot, e.g., "common.buttons.login"). Namespacing your translation keys makes it easier to find a certain key as well.
Do not concatenate keys
It's tempting to reuse keys in the template so you can concatenate them to a newly translated value. This might work for the language you're working in, but what if you have to support a language where it does not? An example:
<button type="submit">{{'common.labels.please' | translate}} {{'common.buttons.login' | translate}}</button>
We want a button saying "Please login". This concatenation might work in English, but if you try it in Dutch for example, it gives "Alsjeblieft inloggen", which sounds not so Dutch.
Therefore, it's better to create a new key with its own dedicated translation and use that key instead.
Use pluralization
Don't implement pluralization yourself in an Angular project by using *ngIf
or another implementation. Make use of NgPlural. Also, do not concatenate a number with the translation key, e.g., 5 {{ common.labels.days | translate }}
, but pass it as a parameter to the translation key if the library you use allows it. It will provide much more flexibility when you start integrating multiple languages.
Namespacing
Namespacing your translation keys is not an easy task. Do you group in pages? Do you group in components? Do you group by type (labels, buttons, etc.)? This is something you should agree upon as early as possible. It will not always fit, but just be sure the team agrees on all categories. If you don't know how to begin, this structure works well most of the time:
{
// common
"common.buttons.test": "",
"common.labels.test": "",
"common.titles.test": "",
"common.messages.test": "",
// component specific
"components.userList.buttons.test": "",
"components.userList.labels.test": "",
"components.userList.titles.test": "",
"components.userList.messages.test": "",
// page specific
"pages.home.metadata.description": ""
}
Note: This is namespaced in the key, but it might as well be nested.
A common
namespace is used for translations that are reused often, like "Save", "Cancel", "Ok",...
{
"common.buttons.ok": "Ok",
"common.buttons.cancel": "Cancel",
"common.labels.createdBy": "Created by {{name}}",
"common.titles.edit": "Edit",
"common.messages.required": "This field is required"
}
The components
namespace is used for translations per component. That way, translations are easily found. The downside of this approach is that when the component changes its name, the namespace should be renamed as well. It's easier to create component specific translation keys instead of keys for an entire page. This is because components can be reused, which can cause confusion when the component of a specific page is reused on another page (which page specific label would you use then?).
Note: If a translation can be reused over several components, put it in the
common
namespace and use that key instead. That way, there won't be duplicates of the same translation. Some people may say that according to UX, the translation should be different (e.g., instead of a generic "save", use "save user" and "save settings"), and they are right. In that case, it should be in thecomponents
namespace. But if that's not the case (even if it ever may be), just use thecommon
namespace (principle of YAGNI).
{
"components.confirmDeleteDialog.buttons.ok": "Ok, delete",
"components.confirmDeleteDialog.buttons.cancel": "No, do not delete",
"components.confirmDeleteDialog.titles.confirm": "About to delete {{name}}",
"components.confirmDeleteDialog.messages.confirm": "Deleting the user will imply that he will no longer has access to the application. Do you want to continue?",
"components.registerForm.labels.firstName": "What is your first name?",
"components.registerForm.labels.lastName": "What is your last name?"
}
There is also the pages
namespace. This namespace is used for page specific translations. What comes to mind is metadata, things like the page title, page description,... Anything that is truly specific to a page can fit under this namespace.
{
"pages.home.metadata.description": "Building ecological homes with the future in mind.",
"pages.home.titles.pageTitle": "Home",
"pages.about.metadata.description": "Homes of the future is a new brand started in 2023 and available for all your ecological building needs.",
"pages.about.titles.pageTitle": "About"
}
For the common
and components
namespaces, there is a subdivision of namespaces:
- buttons are for button texts.
- labels are for single-line labels (e.g., labels next to an input field).
- titles are for headers (e.g., title of a dialog).
- messages are for multiline texts (e.g., paragraphs, dialog text, helper text).
This being said, you have reached the end of this blog. Coping with i18n can be hard sometimes, especially because the needs of the projects can change. I hope this guide will alleviate some of the pain that comes with i18n.
If you have any questions, feel free to contact me!