2020-08-11 01:47:54 +02:00
|
|
|
# Internationalization for developers
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
Zulip, like many popular applications, is designed with
|
|
|
|
internationalization (i18n) in mind, which means users can fully use
|
2021-01-28 09:05:45 +01:00
|
|
|
the Zulip UI in their preferred language.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
This article aims to teach Zulip contributors enough about
|
|
|
|
internationalization and Zulip's tools for it so that they can make
|
2021-08-20 21:53:28 +02:00
|
|
|
correct decisions about how to tag strings for translation. A few
|
2019-06-05 02:12:56 +02:00
|
|
|
principles are important in how we think about internationalization:
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- Our goal is for **all end-user facing strings** in Zulip to be
|
2019-06-05 02:12:56 +02:00
|
|
|
tagged for translation in both [HTML templates](#html-templates) and
|
2021-08-20 21:53:28 +02:00
|
|
|
code, and our linters attempt to enforce this. There are some
|
2019-06-05 02:12:56 +02:00
|
|
|
exceptions: we don't tag strings in Zulip's landing pages
|
2023-03-23 04:51:28 +01:00
|
|
|
(e.g. /features/) and other documentation (e.g. /help/) for
|
2019-06-05 02:12:56 +02:00
|
|
|
translation at this time (though we do aim for those pages to be
|
|
|
|
usable with tools like Google Translate).
|
2021-08-20 21:45:39 +02:00
|
|
|
- Translating all the strings in Zulip for a language and maintaining
|
2019-06-05 02:12:56 +02:00
|
|
|
that translation is a lot of work, and that work scales with the
|
2021-08-20 21:53:28 +02:00
|
|
|
number of strings tagged for translation in Zulip. For this reason,
|
2019-06-05 02:12:56 +02:00
|
|
|
we put significant effort into only tagging for translation content
|
|
|
|
that will actually be displayed to users, and minimizing unnecessary
|
|
|
|
user-facing strings in the product.
|
2021-08-20 21:45:39 +02:00
|
|
|
- In order for a translated user experience to be good, every UI
|
2019-06-05 02:12:56 +02:00
|
|
|
element needs to be built in a way that supports i18n.
|
2021-08-20 21:45:39 +02:00
|
|
|
- This is more about string consistency in general, but we have a
|
2019-06-05 02:12:56 +02:00
|
|
|
"Sentence case" [capitalization
|
2022-02-16 01:39:15 +01:00
|
|
|
policy](translating.md#capitalization) that we enforce using linters
|
2019-06-05 02:12:56 +02:00
|
|
|
that check all strings tagged for translation in Zulip.
|
|
|
|
|
2021-08-20 21:53:28 +02:00
|
|
|
This article aims to provide a brief introduction. We recommend the
|
2019-06-05 02:12:56 +02:00
|
|
|
[EdX i18n guide][edx-i18n] as a great resource for learning more about
|
|
|
|
internationalization in general; we agree with essentially all of
|
|
|
|
their style guidelines.
|
|
|
|
|
2024-03-20 03:17:36 +01:00
|
|
|
[edx-i18n]: https://docs.openedx.org/en/latest/developers/references/developer_guide/internationalization/i18n.html
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
## Key details about human language
|
|
|
|
|
2021-01-28 09:05:45 +01:00
|
|
|
There are a few critical details about human language that are important
|
2019-06-05 02:12:56 +02:00
|
|
|
to understand when implementing an internationalized application:
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
- **Punctuation** varies between languages (e.g. Japanese doesn't use
|
2021-08-20 21:53:28 +02:00
|
|
|
`.`s at the end of sentences). This means that you should always
|
2019-06-05 02:12:56 +02:00
|
|
|
include end-of-sentence symbols like `.` and `?` inside the
|
|
|
|
to-be-translated strings, so that translators can correctly
|
|
|
|
translate the content.
|
2021-08-20 21:45:39 +02:00
|
|
|
- **Word order** varies between languages (e.g. some languages put
|
2021-08-20 21:53:28 +02:00
|
|
|
subjects before verbs, others the other way around). This means
|
2022-02-08 00:13:33 +01:00
|
|
|
that **concatenating translatable strings** produces broken results
|
2019-06-05 02:12:56 +02:00
|
|
|
(more details with examples are below).
|
2021-08-20 21:45:39 +02:00
|
|
|
- The **width of the string needed to express something** varies
|
2019-06-05 02:12:56 +02:00
|
|
|
dramatically between languages; this means you can't just hardcode a
|
|
|
|
button or widget to look great for English and expect it to work in
|
2021-08-20 21:53:28 +02:00
|
|
|
all languages. German is a good test case, as it has a lot of long
|
2019-06-05 02:12:56 +02:00
|
|
|
words, as is Japanese (as character-based languages use a lot less
|
|
|
|
width).
|
2021-08-20 21:45:39 +02:00
|
|
|
- This is more about how i18n tooling works, but in code, the
|
2020-05-27 22:56:05 +02:00
|
|
|
translation function must be passed the string to translate, not a
|
2021-08-20 21:53:28 +02:00
|
|
|
variable containing the target string. Otherwise, the parsers that
|
2020-05-27 22:56:05 +02:00
|
|
|
extract the strings in a project to send to translators will not
|
|
|
|
find your string.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
There's a lot of other interesting differences that are important for
|
2021-05-10 07:02:14 +02:00
|
|
|
i18n (e.g. Zulip has a "full name" field rather than "first name" and
|
|
|
|
"last name" because different cultures order the surnames and given
|
2019-06-05 02:12:56 +02:00
|
|
|
names differently), but the above issues are likely to be relevant to
|
|
|
|
most people working on Zulip.
|
|
|
|
|
|
|
|
## Translation process
|
|
|
|
|
|
|
|
The end-to-end tooling process for translations in Zulip is as follows.
|
|
|
|
|
|
|
|
1. The strings are marked for translation (see sections for
|
|
|
|
[backend](#backend-translations) and
|
|
|
|
[frontend](#frontend-translations) translations for details on
|
|
|
|
this).
|
|
|
|
|
2021-09-08 00:23:24 +02:00
|
|
|
2. Translation resource files are created using the
|
|
|
|
`./manage.py makemessages` command. This command will create, for
|
|
|
|
each language, a resource file called `translations.json` for the
|
|
|
|
frontend strings and `django.po` for the backend strings.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
The `makemessages` command is idempotent in that:
|
|
|
|
|
|
|
|
- It will only delete singular keys in the resource file when they
|
|
|
|
are no longer used in Zulip code.
|
|
|
|
- It will only delete plural keys (see below for the documentation
|
|
|
|
on plural translations) when the corresponding singular key is
|
|
|
|
absent.
|
|
|
|
- It will not override the value of a singular key if that value
|
|
|
|
contains a translated text.
|
|
|
|
|
|
|
|
3. Those resource files are uploaded to Transifex by a maintainer using the
|
|
|
|
`./tools/i18n/push-translations` command (which invokes a Transifex
|
|
|
|
API tool, `tx push`, internally).
|
|
|
|
|
2021-08-20 21:53:28 +02:00
|
|
|
4. Translators translate the strings in the Transifex UI. (In theory,
|
2019-06-05 02:12:56 +02:00
|
|
|
it's possible to translate locally and then do `tx push`, but
|
|
|
|
because our workflow is to sync translation data from Transifex to
|
|
|
|
Zulip, making changes to translations in Zulip risks having the
|
|
|
|
changes blown away by a data sync, so that's only a viable model
|
|
|
|
for a language that has no translations yet).
|
|
|
|
|
|
|
|
5. The translations are downloaded back into the codebase by a
|
|
|
|
maintainer, using `tools/i18n/sync-translations` (which invokes the
|
|
|
|
Transifex API tool, `tx pull`, internally).
|
|
|
|
|
|
|
|
If you're interested, you may also want to check out the [translators'
|
2022-02-16 01:39:15 +01:00
|
|
|
workflow](translating.md#translators-workflow), just so you have a
|
2019-06-05 02:12:56 +02:00
|
|
|
sense of how everything fits together.
|
|
|
|
|
|
|
|
## Translation resource files
|
|
|
|
|
2021-01-28 09:05:45 +01:00
|
|
|
All the translation magic happens through resource files, which hold
|
2019-06-05 02:12:56 +02:00
|
|
|
the translated text. Backend resource files are located at
|
2019-07-02 22:38:09 +02:00
|
|
|
`locale/<lang_code>/LC_MESSAGES/django.po`, while frontend
|
2019-06-05 02:12:56 +02:00
|
|
|
resource files are located at
|
2019-07-02 22:38:09 +02:00
|
|
|
`locale/<lang_code>/translations.json` (and mobile at
|
2019-06-05 02:12:56 +02:00
|
|
|
`mobile.json`).
|
|
|
|
|
|
|
|
These files are uploaded to [Transifex][], where they can be translated.
|
|
|
|
|
|
|
|
## HTML Templates
|
|
|
|
|
|
|
|
Zulip makes use of the [Jinja2][] templating system for the backend
|
|
|
|
and [Handlebars][] for the frontend. Our [HTML templates][html-templates]
|
|
|
|
documentation includes useful information on the syntax and
|
|
|
|
behavior of these systems.
|
|
|
|
|
|
|
|
## Backend translations
|
|
|
|
|
2020-10-21 09:00:51 +02:00
|
|
|
### Jinja2 templates
|
|
|
|
|
2019-06-05 02:12:56 +02:00
|
|
|
All user-facing text in the Zulip UI should be generated by an Jinja2 HTML
|
|
|
|
template so that it can be translated.
|
|
|
|
|
|
|
|
To mark a string for translation in a Jinja2 template, you
|
|
|
|
can use the `_()` function in the templates like this:
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```jinja
|
2019-06-05 02:12:56 +02:00
|
|
|
{{ _("English text") }}
|
|
|
|
```
|
|
|
|
|
|
|
|
If a piece of text contains both a literal string component and variables,
|
|
|
|
you can use a block translation, which makes use of placeholders to
|
2021-08-20 21:53:28 +02:00
|
|
|
help translators to translate an entire sentence. To translate a
|
2021-11-30 22:01:05 +01:00
|
|
|
block, Jinja2 uses the [trans][trans] tag. So rather than writing
|
2019-06-05 02:12:56 +02:00
|
|
|
something ugly and confusing for translators like this:
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```jinja
|
2019-06-05 02:12:56 +02:00
|
|
|
# Don't do this!
|
|
|
|
{{ _("This string will have") }} {{ value }} {{ _("inside") }}
|
|
|
|
```
|
|
|
|
|
|
|
|
You can instead use:
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```jinja
|
2019-06-05 02:12:56 +02:00
|
|
|
{% trans %}This string will have {{ value }} inside.{% endtrans %}
|
|
|
|
```
|
|
|
|
|
2020-10-21 09:00:51 +02:00
|
|
|
### Python
|
|
|
|
|
2019-06-05 02:12:56 +02:00
|
|
|
A string in Python can be marked for translation using the `_()` function,
|
|
|
|
which can be imported as follows:
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```python
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
2019-06-05 02:12:56 +02:00
|
|
|
```
|
|
|
|
|
2021-08-20 21:53:28 +02:00
|
|
|
Zulip expects all the error messages to be translatable as well. To
|
2021-07-04 10:00:55 +02:00
|
|
|
ensure this, the error message passed to `JsonableError`
|
|
|
|
should always be a literal string enclosed by `_()`
|
2019-06-05 02:12:56 +02:00
|
|
|
function, e.g.:
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```python
|
2021-05-10 07:02:14 +02:00
|
|
|
JsonableError(_('English text'))
|
2019-06-05 02:12:56 +02:00
|
|
|
```
|
|
|
|
|
2020-10-21 09:11:13 +02:00
|
|
|
If you're declaring a user-facing string at top level or in a class, you need to
|
2021-04-16 00:57:30 +02:00
|
|
|
use `gettext_lazy` instead, to ensure that the translation happens at
|
2020-10-21 09:11:13 +02:00
|
|
|
request-processing time when Django knows what language to use, e.g.:
|
|
|
|
|
|
|
|
```python
|
|
|
|
from zproject.backends import check_password_strength, email_belongs_to_ldap
|
|
|
|
|
2021-04-16 00:57:30 +02:00
|
|
|
AVATAR_CHANGES_DISABLED_ERROR = gettext_lazy("Avatar changes are disabled in this organization.")
|
2020-10-21 09:11:13 +02:00
|
|
|
|
|
|
|
def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
|
|
|
...
|
|
|
|
```
|
|
|
|
|
|
|
|
```python
|
|
|
|
class Realm(models.Model):
|
|
|
|
MAX_REALM_NAME_LENGTH = 40
|
|
|
|
MAX_REALM_SUBDOMAIN_LENGTH = 40
|
|
|
|
|
|
|
|
...
|
|
|
|
...
|
|
|
|
|
2024-05-20 18:20:53 +02:00
|
|
|
STREAM_EVENTS_NOTIFICATION_TOPIC = gettext_lazy("channel events")
|
2020-10-21 09:11:13 +02:00
|
|
|
```
|
2020-10-18 23:33:47 +02:00
|
|
|
|
2022-02-08 00:13:33 +01:00
|
|
|
To ensure we always internationalize our JSON error messages, the
|
2020-10-18 23:33:47 +02:00
|
|
|
Zulip linter (`tools/lint`) attempts to verify correct usage.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
## Frontend translations
|
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
We use the [FormatJS][] library for frontend translations when dealing
|
2019-06-05 02:12:56 +02:00
|
|
|
with [Handlebars][] templates or JavaScript.
|
|
|
|
|
|
|
|
To mark a string translatable in JavaScript files, pass it to the
|
2021-04-10 09:38:17 +02:00
|
|
|
`intl.formatMessage` function, which we alias to `$t` in `intl.js`:
|
2019-06-05 02:12:56 +02:00
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
```js
|
2021-05-10 07:02:14 +02:00
|
|
|
$t({defaultMessage: "English text"})
|
2019-06-05 02:12:56 +02:00
|
|
|
```
|
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
The string to be translated must be a constant literal string, but
|
|
|
|
variables can be interpolated by enclosing them in braces (like
|
|
|
|
`{variable}`) and passing a context object:
|
2019-06-05 02:12:56 +02:00
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
```js
|
|
|
|
$t({defaultMessage: "English text with a {variable}"}, {variable: "Variable value"})
|
2019-06-05 02:12:56 +02:00
|
|
|
```
|
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
FormatJS uses the standard [ICU MessageFormat][], which includes
|
|
|
|
useful features such as plural translations.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
`$t` does not escape any variables, so if your translated string is
|
|
|
|
eventually going to be used as HTML, use `$t_html` instead.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
```js
|
|
|
|
$("#foo").html(
|
|
|
|
$t_html({defaultMessage: "HTML with a {variable}"}, {variable: "Variable value"})
|
|
|
|
);
|
2019-06-05 02:12:56 +02:00
|
|
|
```
|
|
|
|
|
2021-04-16 04:30:35 +02:00
|
|
|
The only HTML tags allowed directly in translated strings are the
|
|
|
|
simple HTML tags enumerated in `default_html_elements`
|
2023-03-11 08:13:37 +01:00
|
|
|
(`web/src/i18n.ts`) with no attributes. This helps to avoid
|
2021-08-20 21:53:28 +02:00
|
|
|
exposing HTML details to translators. If you need to include more
|
2021-04-16 04:30:35 +02:00
|
|
|
complex markup such as a link, you can define a custom HTML tag
|
|
|
|
locally to the translation:
|
|
|
|
|
|
|
|
```js
|
|
|
|
$t_html(
|
|
|
|
{defaultMessage: "<b>HTML</b> linking to the <z-link>login page</z-link>"},
|
2022-11-03 20:14:03 +01:00
|
|
|
{"z-link": (content_html) => `<a href="/login/">${content_html.join("")}</a>`},
|
2021-04-16 04:30:35 +02:00
|
|
|
)
|
|
|
|
```
|
|
|
|
|
2019-06-05 02:12:56 +02:00
|
|
|
### Handlebars templates
|
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
For translations in Handlebars templates we also use FormatJS, through two
|
2021-08-20 21:53:28 +02:00
|
|
|
Handlebars [helpers][] that Zulip registers. The syntax for simple strings is:
|
2019-06-05 02:12:56 +02:00
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```html+handlebars
|
2021-05-10 07:02:14 +02:00
|
|
|
{{t 'English text' }}
|
2023-03-16 00:47:01 +01:00
|
|
|
|
|
|
|
{{t 'Block of English text with a {variable}.' }}
|
2019-06-05 02:12:56 +02:00
|
|
|
```
|
|
|
|
|
2021-04-10 09:38:17 +02:00
|
|
|
If you are passing a translated string to a Handlebars partial, you can use:
|
2020-04-16 09:05:56 +02:00
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```html+handlebars
|
2020-04-16 09:05:56 +02:00
|
|
|
{{> template_name
|
2021-05-10 07:02:14 +02:00
|
|
|
variable_name=(t 'English text')
|
2020-04-16 09:05:56 +02:00
|
|
|
}}
|
|
|
|
```
|
|
|
|
|
2023-03-16 00:47:01 +01:00
|
|
|
The syntax for HTML strings is:
|
2019-06-05 02:12:56 +02:00
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
<!-- The html+handlebars lexer fails to lex the single braces. -->
|
2021-08-20 22:54:08 +02:00
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```text
|
2021-04-14 03:04:02 +02:00
|
|
|
{{#tr}}
|
2023-03-16 00:47:01 +01:00
|
|
|
<p>Block of English text.</p>
|
2019-06-05 02:12:56 +02:00
|
|
|
{{/tr}}
|
|
|
|
|
2021-04-14 03:04:02 +02:00
|
|
|
{{#tr}}
|
2023-03-16 00:47:01 +01:00
|
|
|
<p>Block of English text with a {variable}.</p>
|
2019-06-05 02:12:56 +02:00
|
|
|
{{/tr}}
|
|
|
|
```
|
|
|
|
|
2021-08-20 22:54:08 +02:00
|
|
|
Just like in JavaScript code, variables are enclosed in _single_
|
2021-08-20 21:53:28 +02:00
|
|
|
braces (rather than the usual Handlebars double braces). Unlike in
|
2021-04-10 09:38:17 +02:00
|
|
|
JavaScript code, variables are automatically escaped by our Handlebars
|
|
|
|
helper.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
Handlebars expressions like `{{variable}}` or blocks like
|
|
|
|
`{{#if}}...{{/if}}` aren't permitted inside a `{{#tr}}...{{/tr}}`
|
|
|
|
translated block, because they don't work properly with translation.
|
|
|
|
The Handlebars expression would be evaluated before the string is
|
2021-04-10 09:38:17 +02:00
|
|
|
processed by FormatJS, so that the string to be translated wouldn't be
|
2021-08-20 21:53:28 +02:00
|
|
|
constant. We have a linter to enforce that translated blocks don't
|
2022-06-27 22:35:01 +02:00
|
|
|
contain Handlebars.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
2021-04-16 04:30:35 +02:00
|
|
|
Restrictions on including HTML tags in translated strings are the same
|
2021-08-20 21:53:28 +02:00
|
|
|
as in JavaScript. You can insert more complex markup using a local
|
2021-04-16 04:30:35 +02:00
|
|
|
custom HTML tag like this:
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```html+handlebars
|
2021-04-16 04:30:35 +02:00
|
|
|
{{#tr}}
|
|
|
|
<b>HTML</b> linking to the <z-link>login page</z-link>
|
|
|
|
{{#*inline "z-link"}}<a href="/login/">{{> @partial-block}}</a>{{/inline}}
|
|
|
|
{{/tr}}
|
|
|
|
```
|
|
|
|
|
2019-06-05 02:12:56 +02:00
|
|
|
## Transifex config
|
|
|
|
|
|
|
|
The config file that maps the resources from Zulip to Transifex is
|
|
|
|
located at `.tx/config`.
|
|
|
|
|
|
|
|
## Transifex CLI setup
|
|
|
|
|
|
|
|
In order to be able to run `tx pull` (and `tx push` as well, if you're a
|
2022-12-08 09:31:12 +01:00
|
|
|
maintainer), you have to specify your Transifex API Token, [generated in
|
|
|
|
Transifex's web interface][transifex-api-token], in a config file located at
|
|
|
|
`~/.transifexrc`.
|
2019-06-05 02:12:56 +02:00
|
|
|
|
|
|
|
You can find details on how to set it up [here][transifexrc], but it should
|
|
|
|
look similar to this (with your credentials):
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
```ini
|
2019-06-05 02:12:56 +02:00
|
|
|
[https://www.transifex.com]
|
2022-12-08 09:31:12 +01:00
|
|
|
rest_hostname = https://rest.api.transifex.com
|
|
|
|
token = 1/abcdefg...
|
2019-06-05 02:12:56 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
This basically identifies you as a Transifex user, so you can access your
|
|
|
|
organizations from the command line.
|
|
|
|
|
2021-08-20 22:54:08 +02:00
|
|
|
[jinja2]: http://jinja.pocoo.org/
|
|
|
|
[handlebars]: https://handlebarsjs.com/
|
2021-11-30 22:01:05 +01:00
|
|
|
[trans]: https://jinja.palletsprojects.com/en/3.0.x/extensions/#i18n-extension
|
2021-08-20 22:54:08 +02:00
|
|
|
[formatjs]: https://formatjs.io/
|
|
|
|
[icu messageformat]: https://formatjs.io/docs/intl-messageformat
|
2020-02-25 20:48:43 +01:00
|
|
|
[helpers]: https://handlebarsjs.com/guide/block-helpers.html
|
2023-06-13 02:06:32 +02:00
|
|
|
[transifex]: https://www.transifex.com
|
|
|
|
[transifex-api-token]: https://app.transifex.com/user/settings/api/
|
2019-06-05 02:12:56 +02:00
|
|
|
[transifexrc]: https://docs.transifex.com/client/client-configuration#transifexrc
|
2022-02-16 01:39:15 +01:00
|
|
|
[html-templates]: ../subsystems/html-css.md#html-templates
|