Internationalization deprecated
Providing software for users in the whole world means providing it in multiple languages. This consists of two steps:
- Internationalization (i18n): Making the software international, i. e. preparing it to be adapted to different languages and locations.
- Localization (L10n): Adapting the software to a particular language and/or location.
The Open-Xchange platform offers several facilities to simplify both parts. The L10n part is mostly a concern for translators. Open-Xchange facilities for that consist mainly of using a well-established format for translations: GNU Gettext Portable Object (PO) files. This allows translators to use existing dedicated translation tools or a simple UTF-8 capable text editor.
The i18n part is what software developers will be mostly dealing with and is what the rest of this document describes.
Translation
The main part of i18n is the translation of various text strings which are presented to the user. For this purpose, the Open-Xchange platform provides the virtual module gettext through the vite plugin @open-xchange/vite-plugin-gettext. Individual translation files can be specified as module dependency of the form gettext?dictionary=module_name. The resulting module API is a function which can be called to translate a string using the specified translation files:
import gt from 'gettext'
alert(gt('Hello, world!'))
import gt from 'gettext?dictionary=com.example/example'
alert(gt('Hello, world!'))
A file named i18n.pot is created by the build system (another file path is possible via plugin options). You will need to run:
pnpm build
This will generate this file within the bundle. It will contain an entry for every call to one of gettext functions:
#: apps/com.example/example.js:4
msgid "Hello, world!"
msgstr ""
This file can be sent to the translators and during the translation process, one PO file for each language will be created. The PO files in the directory i18n should contain the translated entry:
#: apps/com.example/example.js:4
msgid "Hello, world!"
msgstr "Hallo, Welt!"
During the next build, the entries are copied from the central PO files into individual translation files. In our example, this would be apps/com.example/example.de_DE.js. Because of the added language ID, translation files can usually have the same name as the corresponding main module. Multiple related modules should use the same translation file to avoid the overhead of too many small translation files.
Most modules will require more complex translations than can be provided by a simple string lookup. To handle some of these cases, the gettext module provides traditional methods in addition to being a callable function. Other cases are handled by the build system.
Composite Phrases
In many cases, the translated texts will not be static, but contain dynamic values as parts of a sentence. The straight-forward approach of translating parts of the sentence individually and then using string concatenation to compose the final text is a BAD idea. Different languages have different sentence structures, and if there are multiple dynamic values, their order might need to be reversed in some languages, and not reversed in others.
The solution is to translate entire sentences and then to use the gt function to insert dynamic values:
//#. %1$s is the given name
//#. %2$s is the family name
alert(gt('Welcome, %1$s %2$s!', firstName, lastName))
Internally, the _.printf function is used to expand the placeholders, so the above is equivalent to:
//#. %1$s is the given name
//#. %2$s is the family name
const pattern = gt('Welcome, %1$s %2$s!')
alert(_.printf(pattern, firstName, lastName))
and results in:
#. %1$s is the given name
#. %2$s is the family name
msgid "Welcome, %1$s, %2$s!"
msgstr ""
Comments
As shown in the examples above, it is possible to add comments for translators by starting a comment with the characters #.. Such comments must be placed immediately before the gt function call, or in the line above. All such gt calls should have comments explaining every format specifier.
Examples of correct usage of comments:
alert(/*#. comment */ gt('Welcome!'))
//#. comment
alert(gt('Welcome!'))
//#. multi
//#. line
//#. comment
alert(gt('Welcome!'))
Examples of wrong usage of comments:
/*#. comment */ alert(gt('Welcome!'))
//#. comment
const x = 42
alert(gt('Welcome!'))
Advanced gettext Functions
There are several other functions which are required to cover all typical translation scenarios.
Contexts
Sometimes, the same English word or phrase has multiple meanings and must be translated differently depending on context. To be able to tell the individual translations apart, the function gt.pgettext ("p" stands for "particular") should be used instead of calling gt directly. It takes the context as the first parameter and the text to translate as the second parameter:
alert(gt.pgettext('label', 'Title'))
alert(gt.pgettext('salutation', 'Title'))
Results in:
msctxt "label"
msgid "Title"
msgstr "Überschrift"
msctxt "salutation"
msgid "Title"
msgstr "Anrede"
Plural Forms
In the case of numbers, the rules to select the proper plural form can be very complex. With the exception of languages with no separate plural forms, English is the second simplest language in this respect, having only two plural forms: singular and plural. Other languages can have no plural form at all, or up to four forms, and theoretically even more.
The functions gt.ngettext and gt.npgettext (for a combination of plural forms with contexts) can select the proper plural form by using a piece of executable code embedded in the header of a PO file:
//#. %1$d is the number of mails
const pattern = gt.ngettext('You have %1$d mail.', 'You have %1$d mails.', n)
alert(_.printf(pattern, n))
The function gt.ngettext accepts three parameters: the English singular and plural forms and a "count" parameter which determines the chosen plural form. It returns the appropriate translated text without processing the placeholders. The function gt.npgettext adds a context parameter before the others, similar to gt.pgettext.
They would usually be used in combination with _.printf to insert the actual number into the translated string. However, all gettext functions are able to process placeholders by themselves, so the following example is equivalent and preferred:
//#. %1$d is the number of mails
alert(gt.ngettext('You have %1$d mail.', 'You have %1$d mails.', n, n)
The above examples result in the following entry:
#. %1$d is the number of mails
msgid "You have %1$d mail"
msgid_plural "You have %1$d mails"
msgstr[0] ""
msgstr[1] ""
The number of msgstr[N] lines is determined by the number of plural forms in each language. This number is specified in the header of each PO file, together with the code to compute the index of the correct plural form from the supplied number.
Example with context parameter:
alert(gt.npgettext('context', 'You have %1$d mail.', 'You have %1$d mails.', n, n))
All functions can take additional format parameters following the regular parameters described above:
alert(gt.ngettext('Welcome %1$s! You have %2$d mail.', 'Welcome %1$s! You have %2$d mails.', n, userName, n))
alert(gt.npgettext('context', 'Welcome %1$s! You have %2$d mail.', 'Welcome %1$s! You have %2$d mails.', n, userName, n))
Singular, Plural, Exactly One
As already mentioned, there are languages that have different plural forms depending on the count, or that use their singular form for other counts but 1 (such as 21, or 101). Remember that "singular" is not the same as "a single entity" in these languages but has to be seen as just one plural form among others.
Do not write out "1" or "one" in the singular form, or use another specific sentence for a single entity when using the plural functions. Otherwise, for example, the resulting text in the UI may tell about "one email" in some languages when there are 101 emails actually.
// WRONG!
alert(gt.ngettext('You have 1 mail.', 'You have %1$d mails.', n, n))
// Correct
alert(gt.ngettext('You have %1$d mail.', 'You have %1$d mails.', n, n))
In cases where a special term is needed for "exactly 1", separate that string from the plural function:
// WRONG!
alert(gt.ngettext('Every day.', 'Every %1$d days.', n, n))
// Correct
alert((n === 1) ? gt('Every day.') : gt.ngettext('Every %1$d day.', 'Every %1$d days.', n, n))
Always use the plural functions even if it is known that the count cannot be 1.
// WRONG!
if (n > 1) alert(gt.gettext('You have %1$d mails.', n))
// Correct
if (n > 1) alert(gt.ngettext('You have %1$d mail.', 'You have %1$d mails.', n, n))
This will still always pick the plural form in English, but will pick the correct forms in other languages with more complex rules.
Do not use the plural functions at all for sentences using a generic plural without count:
// WRONG!
alert(gt.ngettext('Unable to upload file', 'Unable to upload files', n))
// Correct
alert((n === 1) ? gt('Unable to upload file') : gt('Unable to upload files'))