Internationalization (i18n)
Internationalization (sometimes abbreviated as "i18n") is a process for supporting different languages and locales in software. This page describes how is that handled in Agama.
Each component need to solve this problem separately, see more details for each part in the details below.
Using translations
Users have two ways how to change the used language in the Agama interface.
Language selector
The sidebar of the web UI contains a component that allows to change the language. It was introduced in Agama 5 and it is the recommended way. The list of supported languages is read from the languages.json file. Check the The web front-end for further details.
URL query parameter
When using a remote installation it is possible to set the used language via an URL query parameter. This is an expert option.
To change the language append the ?lang=<locale>
query to the URL when accessing the Agama
installer. The locale
string uses the RFC 5646 but
the usual Linux locale format is supported too, e.g. cs_CZ
.
It is the user responsibility to use a correct locale name. When using a wrong name the translations might be broken, displayed only partially or even not at all.
Changing the language causes reloading the page, in some situations this could cause losing some entered values on the current page. Therefore it is recommended to change the language at the very beginning.
Translations
For translation process Agama uses Weblate tool running at the openSUSE instance.
The workflow
The basic translation workflow looks like this:
- The translatable texts are marked for translation in the source code, usually with the
_()
function or something similar - The translatable string are collected into a POT file which is uploaded to the staging agama-weblate GitHub repository
- The POT file is loaded by the Weblate into the agama project
- Translators then can translate the English texts into other languages
- Weblate pushes back the translations in the PO files back to the agama-weblate repository
- The translations in the PO files are regularly copied to the main repository using pull requests
- The PO files are processed during build so the translations can be used later at runtime
Staging translation repository
The special agama-weblate repository works like a buffer between the Agama sources and the Weblate tool.
We do not want to spam the Weblate tool with every trivial change in the texts and the other way round, we do not need to get dozen commits from the Weblate every day with updated translations. This would be especially annoying before releasing a new version where we might want to accept only unnecessary changes to not break something at the very last minute.
The agama-weblate repository uses webhooks to notify the Weblate instance of any change. Changes in the repository should be visible in Weblate in matter of seconds.
Synchronization
The content between the main agama and the translation agama-weblate GitHub repositories is synchronized automatically using the GitHub Actions.
Uploading translatable texts
Collecting and uploading the translatable texts is done by the weblate-update-pot.yml GitHub action.
- It checks out both
agama
andagama-weblate
repositories - It runs a script which extracts the translatable strings in the
agama
repository into a POT file - The POT file is copied to the
agama-weblate
repository and committed to the GitHub repository
This action is run daily, but it can be started manually if needed. Go to the weblate-update-pot.yml action detail and use the "Run workflow" option at the top of the page.
The code compares the old POT file with the new one and if there is no change besides the timestamps
in the file it is not uploaded to agama-weblate
.
Downloading updated translations
The translations from the agama-weblate
repository are merged back by the
weblate-merge-po.yml
GitHub action.
- It checks out both
agama
andagama-weblate
repositories - It copies the PO files from the
agama-weblate
to theagama
repository - It updates the
languages.json
file with the list of supported languages. - It creates a pull request with the changes
If there are no changes besides the timestamps in the PO files the pull request is not created.
Weblate configuration
The Agama Weblate project defines a separate translation component for each Agama part (the web front-end, the D-Bus back-end and the command line interface).
For reading the translations it uses the agama-weblate GitHub repository, but for the source code locations it uses the original agama repository. That means after clicking the source location link in the Weblate you will see the correct source location in the other repository.
Plug-ins
The Weblate components use the msgmerge plug-in which automatically updates all PO files to match the POT file. That means after adding or updating any translatable text the changes are automatically applied to all existing PO files and translators can translate the new or updated texts immediately.
Technical details
This part describes technical and implementation details. It also describes the translation process for developers.
The web front-end
Most of the translatable texts which can user see in Agama are defined in the web front-end. However, the web front-end can display some translated texts coming from the back-end part so it is important to set the same language in both parts and make sure the translations are available there.
The list of supported languages is read from the languages.json. Such a list is automatically refreshed when any of the PO files is updated (see Uploading Translatable Texts).
The update-manifest.py
script is the
responsible for updating the list of supported languages. It goes through all PO files, filters out
the ones with less than 70% of translated content and adds the corresponding locales to the
languages.json
.
The name of the locale is taken from langtable, so be sure to
have it installed. However, when the territory is not include in the PO file (es
instead of
es_es
), the script uses the locales.json
map to determine which
territory to use.
If you need to manually update the languages.json
file, run:
web/share/update-languages.py > web/src/languages.json
Marking texts for translation
The texts are marked for translation using the usual functions like _()
, n_()
and others. It is
similar to the GNU gettext style.
The mechanism of loading and displaying the translation is heavily inspired in Cockpit's approach. The i18n module offers a set of translation functions:
import { _, n_, N_, Nn_ } from "~/i18n";
It is important to use these functions only on string literals! Do not use them with string templates or string concatenation, that will not work properly.
The _
and n_
functions can be used on variables or constants, but their content must marked for
translation using the N_
or Nn_
functions on string literals. See the examples below.
Singular Form
This is the most common way:
const title = _("Storage");
For using translated text in React components you need to use curly braces to switch to plain Javascript in the attributes and the content:
<Section title={_("Storage")}></Section>
or
<Button>{_("Select")}</Button>
For translating long texts you might use multiline string literals:
const description = _(
"Select the device for installing the system. All the \
file systems will be created on the selected device.",
);
If you need to insert some values into the text you should use the formatting function. It is recommended to use translator comments for short or ambiguous texts.
Plural form
If the translated text contains a number or quantity it is good to use a plural form so the translated texts looks naturally.
sprintf(
// the first argument is a singular form used when errors.length === 1,
// the second argument is used in all other cases
n_("%d error found", "%d errors found", errors.length),
errors.length,
);
Although the English language has only a single plural form the translators can use more of them. The translation definition also defines a function that computes which plural form to use for a specific number. See more details about the plural forms in the GNU gettext documentation.
Translating constants
The top-level constants are evaluated too early, at that time the translations are not available yet and the language is not set.
The constant value must be marked with N_()
function which does nothing and then later translated
with the usual _()
function.
const LABELS = Object.freeze({
auto: N_("Auto"),
fixed: N_("Fixed"),
range: N_("Range")
});
export default function Foo() {
...
<label>{_(LABELS[value])}</label>
Formatting texts
For formatting complex texts use the C-like sprintf()
function.
import { sprintf } from "sprintf-js";
sprintf(_("User %s will be created"), user);
See the sprintf-js documentation for more details about the formatting specifiers.
Note: You cannot use HTML tags in the translated texts, React automatically escapes all special
HTML characters like <>&
. That is different to YaST where we can use HTML tags in rich text
messages.
It is recommended to avoid building texts from several parts translated separately. This is error prone because the translators will not see the complete final text and might translate some parts inaccurately.
// do NOT use this! it is difficult to translate the parts correctly
// so they build a nice text after merging
return <div>{_("User ")}<b>{user}</b>{_(" will be created")}</div>
// rather translate a complete text and then split it into parts
// TRANSLATORS: %s will be replaced by the user name
const [msg1, msg2] = _("User %s will be created").split("%s");
...
return <div>{msg1}<b>{user}</b>{msg2}</div>
Text splitting might be quite complex in some cases, but still should be preferred.
// TRANSLATORS: error message, the text in square brackets [] is a clickable link
const [msgStart, msgLink, msgEnd] = _("An error occurred. \
Check the [details] for more information.").split(/[[\]]/);
...
return <p>{msgStart}<a>{msgLink}</a>{msgEnd}</p>;
Building sentences from separate parts might be easy in English, but is some other languages it might much more complex. Always assume that the target language has more complex grammar and rules.
// do NOT use this! it is difficult to translate "enabled" and "not enabled"
// differently in the target language!
const msgNot = enabled ? "" : _("not");
sprintf(_("Service is %s enabled"), msgNot);
// this is better
enabled ? _("Service is enabled") : _("Service is disabled");
TRANSLATORS comments
You can use a special TRANSLATORS
keyword in the comment preceding the text marked for
translation. All comments after the keyword are included in the translations file, this should help
the translator to correctly translate the text.
// this line is NOT included in the PO file
// TRANSLATORS: this line is included in the PO file,
// this line as well
_("Back");
The translators comments should be used especially for short texts to better describe the meaning and the context of the message.
// TRANSLATORS: button label, going back to the main page
_("Back");
Also the formatting placeholders should be always described.
// TRANSLATORS: %s will be replaced by the user name
_("User %s will be created");
The JSX code does not support comments natively, you have to write them in curly braces.
<Button>
{/* TRANSLATORS: button label */}
{_("Remove")}
</Button>
But in the component attributes you can use usual Javascript comments.
<TextInput
id="port"
name="port"
value={data.port || ""}
// TRANSLATORS: network port number
label={_("Port")}
/>
The translators can change the order of the arguments if needed using additional positional
arguments. For example to change the order of the arguments in "Foo %s %s"
they can translate it
as "Bar %2$s %1$s"
.
Missing translations
Here are some code examples which might look correct at the first sight. They even work, no crash. But there are still translation problems with them.
-
Do not use Javascript string templates, that does not work at all (
), use a string formatting function for that (_(`User ${user} will be created`)
sprintf(_("User %s will be created"), user)
). -
Use the translation functions only with string literals without any operators, texts like
will not be extracted correctly to the POT file._("foo" + "bar")
In both cases the strings will not be extracted to the POT file.
ESLint plug-in
There is special ESLint plug-in eslint-plugin-agama-i18n which ensures that a string literal is passed to the translation functions. See more details there.
Testing language
The Agama web interface supports special testing xx
language. That language needs to be selected
manually by adding the ?lang=xx
query string to the server URL. With this setting the _()
and
n_()
translation functions replace all alphabetical characters by the x
symbol.
This can be used for testing to find out the texts which are not marked for translation. If a text
is not replaced by the x
symbols then either it is not marked for translation or it comes from the
back-end and it should be translated by the back-end.
However, this is not perfect. It will replace also the texts mentioned in the missing translations section above but in reality the there would be no translations available for them.
Building the POT file
The translatable strings are extracted from the source files by using the standard xgettext
tool.
There is a helper script build_pot
which defines the needed parameters for the xgettext
tool.
To build the POT file locally just run the script, it will save the output to the agama.pot
file.
Loading web UI translations
The translations are loaded by the <script src="po.js" defer></script>
HTML code in the
index.html file. But there is no
such file in Agama but one po.<LANG>.js
file per language, like po.cs.js
.
The trick is that Agama's web server checks the agamaLang
cookie sent in the HTTP request header
and returns the content from the respective file.
Development server
Because Agama serves the po.js
file a bit differently as described in the
section above we need to implement this logic also in the
development server.
The webpack-po-handler.js file checks the language cookie and uses HTTP redirection to load the respective language file. It uses redirection because the built translation files are only available in the webpack memory. But the result is the same, the browser gets a different content according to the currently configured language.
Back-end locale
The Agama server exposes a uiLocale
configuration option via the /api/l10n/locales/config
endpoint which defines the locale used by all the services.
The uiLocale
property is a single global value shared by all connected clients. That means it is
possible to use only one language for all clients, if more users connect to the server and uses a
different UI language then there will be race conditions and the other users might see the texts
coming from the back-end in another language than expected.
This is a known limitation, we expect that only one user at a time will access the Agama installer or if multiple users use the same server we expect that they will be from the same team or company using the same language.
To check or set the locale you can use the /api/l10n/locales/config
endpoint. Alternatively, you
can check (but not change) the current locale via D-Bus:
sudo busctl --address unix:path=/run/agama/bus get-property org.opensuse.Agama1 \
/org/opensuse/Agama1/Locale org.opensuse.Agama1.Locale UILocale
Back-end translations
The back-end might return texts from external components like libstorage-ng
. Make sure the
translations for those components are also available for Agama, e.g. the libstorage-ng
translations are stored in the libstorage-ng-lang
package or the YaST translations are stored in
yast2-trans-<LANG>
packages.
Troubleshooting
Here are some hints what to do when some untranslated text appears in the Agama UI.
- Check that the text is marked for translation, for a quick verification you might try setting the testing language.
- If the text comes from back-end or the other parts check that the appropriate language package is installed.
- The text should be extracted into the POT file
- The agama.pot in the
agama-weblate
repository is up to date, if not then run the Weblate Update POT Github Action manually. - The text is translated in the Weblate repository.
- The translation is included in the respective PO file in the agama-weblate repository.
- The PO file in the agama repository is up to date, if not the check whether there is an open pull request with the change, if yes then check it and merge, if not then run the Weblate Merge PO GitHub Action manually, then check the created pull request.
- The translated string should be present in the built packages, run
npm build
command and check thedist/po.*.js
files, check the built RPM package. - The translations are loaded by the browser, check the content loaded for the
po.js
file. - Check the current language used in the browser, run
agama.language
command in the development tools console, check theagamaLang
cookie value (rundocument.cookie
command in the console). - Check the language used by the Back-end locale.