-
Notifications
You must be signed in to change notification settings - Fork 88
Internationalization system
The library comes with a internationalization / localisation / translation system (call it whatever you want, I will use "i18n" in this page). It allows you to define all the strings of your app in separate files, outside of the code, and swap the set of strings used depending on the system locale (language + country).
So far, only the Switch platform is able to detect current locale; PC platform is hardcoded to always use the default one.
Hold up! To learn, the best way is to look at the example, which showcases every feature of the system. This way you will see how to use multiple JSON files, nested strings and format arguments. So I recommend to read the documentation to understand how the system works, and to look at the example to understand how to use it.
Note: it is not advised to use the i18n system to translate logs - they should be in English for everyone to be able to diagnose any issue with the app.
The default locale is en-US
.
You start by defining a set of strings, in one or more files. A string is a (localized) piece of text, identified by its name. Every locale has the same strings - the name and hierarchy are the same, but the values of the strings are obviously different (because it's localized).
Don't worry, we'll see later how to add strings to your app.
In the code, you use strings by their names. The system takes care of finding the corresponding text depending on the system current locale.
The lookup order of strings is the following:
- current system language (localized)
- default locale (English string)
- raw string name as a fallback (illegible garbage for users)
Note: If the lookup fails, the raw string name will be returned but an error will be logged. Check the logs to see what went wrong.
Let's consider that you have the string main/hello_world
on the two locales English and French:
- on an English Switch, the lookup will return "Hello world"
- on a French Switch, the lookup will return "Bonjour 🥖"
- on a Spanish Switch, the lookup will also return "Hello world" because you app hasn't been translated to Spanish yet
Please note that the default locale is not mandatory to have in your app, but it's heavily recommended. Having no default language can lead to people having illegible text on the entire app. It's better to have an UI in English than no UI at all.
In our case, no default language would mean that Spanish users see "main/hello_world", which is not a nice greeting.
The following locales are currently supported: ja
, en-US
, en-GB
, fr
, fr-CA
, de
, it
, es
, zh-CN
, zh-Hans
, zh-Hant
, zh-TW
, ko
, nl
, pt
, pt-BR
, ru
, es-419
.
To add translation files, start by creating an i18n
folder in your resources if it's not already here (hint: it should be here, see Reserved borealis strings
below.
Inside i18n
, the file structure is the following:
- one folder by locale
- as many JSON files as you want in each folder - files must end with
.json
- each locale must have the same file names
- the strings must match between the same files of different locales
Each file must contain one top-level JSON "object" (key/value pairs surrounded by {}
). Inside, you can write all your strings - the key is the string name, the value is the translation of that string.
The string names must not contain the following characters: /
, ~
,
(whitespace), #
, $
, which are JSON Pointer reserved keywords. Numbers are fine.
To use a string in your app, you need to reference the file name followed by a slash and the string name. So if your hello world string is in the file main.json
, it would be called main/hello_world
in the code.
You can optionnally nest multiple JSON objects in your translation files. To refer to a nested string in the code, use a slash as a separator.
If you write the following JSON to i18n/en-US/main.json
:
{
"nested": {
"string": "henlo"
},
"simple": "worl"
}
You can access "henlo" by getting main/nested/string
, and "worl" by getting main/simple
.
Then, to translate that, create a main.json
in fr
and write the following content:
{
"nested": {
"string": "salut"
},
"simple": "monde"
}
The brls.json
file contains all the strings reserved by the libs, such as the bottom-right hints ("OK", "Back", "Exit"...). If you don't want your app to look broken, you should copy paste the example i18n
folder in your resources when creating a new project. Then, delete everything but brls.json
in every locale folder. It's tedious, I know, but it allows you to translate those texts if you want to.
Now that you wrote some strings in your JSON files, it's time to actually get them in the code. The i18n API offers two ways to achieve that...
BUT NOT YET! You first have to LOAD the translations, by using i18n::loadTranslations();
. This is not automatically done in the Application
init to allow you to translate the app name.
Once the strings are loaded, you can use the following two functions in the brls::i18n
namespace:
- a
getStr(format, args...)
function:std::string translated = i18n::getStr("main/hello_world");
- a
_i18n
user-defined string literal:std::string translated = "main/hello_world"_i18n;
The main difference is that getStr
supports 🌈 format arguments 🌈. You should use it if you don't like literals, or if the string has format arguments. We'll get there in a minute.
brls::i18n
is annoying to write. You can do namespace i18n = brls::i18n;
to make a shortcut of that namespace called i18n
.
To use the _i18n
literal, you HAVE to "use" the i18n::literals
namespace: using namespace brls::i18n::literals;
You can copy paste one of those tiny pieces of code at the top of every file you need the i18n functions in:
namespace i18n = brls::i18n; // for loadTranslations() and getStr()
using namespace i18n::literals; // for _i18n
namespace i18n = brls::i18n; // for loadTranslations() and getStr()
using namespace brls::i18n::literals; // for _i18n
The format arguments allows you to define "holes" in the strings, that are injected at runtime because you don't know what will be written here. It works exactly the same way as the logger. In fact, they both use the fmt
library.
To use format arguments in your string, put the index of the argument between brackets, like so: {0}
, {1}
. They are called positional arguments because you are explicitely stating which argument goes where in the string... and that is very important for localization.
You don't have to use all arguments in a translation, however you must ALWAYS put all of the arguments in the getStr
function call, regardless of the translation. If you don't do that, the string lookup will fail with the error "argument not found". See the example below if it's not clear.
Oh and also, arguments starts at 0 😉
You can escape the brackets by doubling them: {{}}
. Single brackets don't need to be escaped as they will be ignored by the library.
Let's say that you want to welcome an user using their first and last name: Min Feng.
The English string would be Welcome {0} {1}!
, and you would call the function like so: std::string hello = i18n::getStr("main/hello", firstName, lastName);
. That would display Welcome Min Feng!
.
However, let's consider that you translate your app in Korean. In Korea, you refer to people using their last name first, then their first name. The string would then be 어서 오십시오 {1} {0}
, but the getStr
call STAYS the same. The zero and one are just inverted, to tell the library to put the second argument first, then the first argument. The resolved string would be 어서 오십시오 Feng Min!
.
Now let's say that you want to add a middle name to that string. You change the English string to Welcome {0} {2} {1}
, but you forget to add the corresponding argument in the function call... 💥 it fails with "argument not found" because the library doesn't know what to put instead of the {2}
.
You don't have to change the Korean string and can keep 어서 오십시오 {1} {0}
because you don't have to use all arguments in the string. You can use the first and second one and discard the third one just fine.
The correct call is std::string hello = i18n::getStr("main/hello", firstName, lastName, middleName);
so that the middle name is in third - it will show in between the first and last name in English, and will be hidden in Korean.
Because it can be easy to mess up the i18n files, the library contains a Python linter script that checks that everything is in place for you:
- Stray files and directories in the i18n folder
- Unknown locales
- Invalid JSON files and types
- Presence of the default locale
- Illegal characters in strings names
- Untranslated strings
- Translations of unknown strings
- Duplicated strings
That script is located in scripts/i18n-linter.py
. To use it, you need Python 3 and the pip libraries in requirements.txt
.
The linter takes a single argument, the path to the i18n folder to check: python scripts/i18n-linter.py resources/i18n
.
Here is the list of all warnings and errors generated by the linter, and some help on how to resolve them. Errors will cause your app to malfunction and/or crash, and warnings will cause missing translations in the UI.
Error codes:
- E01: Requested i18n folder doesn't exist, check the command you are running
- E02: Requested i18n folder is not a folder, check the command line you are running
- E05: Invalid JSON data, see error message for the details
- E06: The name of a string contains illegal characters, remove it
- E07: A string is of an invalid type, please make sure that you only use JSON strings for your translations
Warning codes:
- W01: Stray directory in i18n folder, remove it
- W02: Stray file in i18n folder, remove it
- W03: A folder has been found for an unknown locale, look at the supported locales list and make sure there is no typo in the folder name
- W04: The default locale contains a string that is not translated on all locales (untranslated string), add it to the concerned locales or remove the string from the default locale
- W05: A locale contains a translation for a string that is not in the default locale (translation of unknown string), remove the translation or add the string to the default locale
- W06: Default locale is missing from the i18n folder, add it
- W07: A string is present in multiple files, remove all duplicates
Note: the linter stops at the first error it encounters. Solving one error doesn't mean there will not be another one just after - you need to run the script until there is no warning or error to make sure everything is fine.