Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the i18n documentation #740

Merged
merged 2 commits into from
Sep 6, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions doc/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
**Table of Contents**

- [Internationalization (i18n)](#internationalization-i18n)
- [Using Translations](#using-translations)
- [URL Query Parameter](#url-query-parameter)
- [Language Selector](#language-selector)
- [Translations](#translations)
- [The Workflow](#the-workflow)
- [Staging Translation Repository](#staging-translation-repository)
Expand All @@ -11,6 +14,15 @@
- [Downloading Updated Translations](#downloading-updated-translations)
- [Weblate Configuration](#weblate-configuration)
- [Plugins](#plugins)
- [Technical Details](#technical-details)
- [The Web Frontend](#the-web-frontend)
- [Marking Texts for Translation](#marking-texts-for-translation)
- [Singular Form](#singular-form)
- [Plural Form](#plural-form)
- [Translating Constants](#translating-constants)
- [Formatting Texts](#formatting-texts)
- [TRANSLATORS Comments](#translators-comments)
- [Missing Translations](#missing-translations)

---

Expand All @@ -22,6 +34,31 @@ Each Agama part (the web frontend, the D-Bus backend and the command line
interface) needs 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.

### 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 usual Linux locale
format, 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.

### Language Selector

*to be done...*

## Translations

For translation process Agama uses [Weblate](https://weblate.org/) tool running
Expand Down Expand Up @@ -126,3 +163,231 @@ plugin 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 Frontend

Most of the translatable texts which can user see in Agama are defined in the
web frontend. However, the web frontend can display some translated texts coming
from the backend part so it is important to set the same language in both parts
and make sure the translations are available there.

### 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.

Currently Agama uses the Cockpit implementation for loading and displaying
translations. To make this process transparent for developers and to allow easy
change of the implementation there is a simple [i18n.js]( ../web/src/i18n.js)
wrapper which provides the translation functions:

```js
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:

```js
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:

```js
<Section title={_("Storage")}></Section>
```

or

```js
<Button>{_("Select")}</Button>
```

For translating long texts you might use multiline string literals:

```js
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](#formatting-texts). It is recommended to use
[translator comments](#translators-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.

```js
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](
https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html) 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.

```js
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.

```js
import { sprintf } from "sprintf-js";

sprintf(_("User %s will be created"), user)
```

See the [sprintf-js](https://www.npmjs.com/package/sprintf-js) documentation
for more details about the formatting specifiers.

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.

```js
// 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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is wrong, but...


// 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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... this is not much of an improvement to me. I think this is better:

const bold_user = <b>{user}</b>;
return <div>{ sprintf(_("User %s will be created"), bold_user)}</div>

or am I missing a catch related to differences between JSX and strings, <b>foo</b> and "<b>foo</b>"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does not work, that bold_user constant is not a string, it is an object. This would be rendered as:

User [object Object] will be created

To avoid code injection React by default escapes all inserted texts so you cannot use HTML tags in translations:

{sprintf(_("User <b>%s</b> will be created"), "USER")}

is rendered as User &lt;b&gt;USER&lt;/b&gt; will be created HTML so the user will see:

User <b>USER</b> will be created

There is a way to inject unescaped text but it is insecure and not recommended: https://legacy.reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so what I was missing is: in React you cannot make HTML from strings, only from React objects

```

Text splitting might be quite complex in some cases, but still should be
preferred.

```js
// 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>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK here it makes more sense, especially if <a> gets some attributes that we don't want the translator to touch

```

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.

```js
// 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.

```js
// 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.

```js
// TRANSLATORS: button label, going back to the main page
_("Back")
```

Also the formatting placeholders should be always described.

```js
// 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.

```js
<Button>
{/* TRANSLATORS: button label */}
{_("Remove")}
</Button>
```

But in the component attributes you can use usual Javascript comments.

```js
<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
(~~``_(`User ${user} will be created`)``~~), use a string formatting function
for that (`sprintf(_("User %s will be created"), user)`).

- Use the translation functions only with string literals without any operators,
texts like ~~`_("foo" + "bar")`~~ will not be extracted correctly to the POT
file.