2016-04-29 06:55:09 +02:00
|
|
|
# Translating Zulip
|
|
|
|
|
|
|
|
To make Zulip even better for users around the world, the Zulip UI is
|
|
|
|
being translated into a number of major languages, including Spanish,
|
|
|
|
German, French, Chinese, Russian, and Japanese, with varying levels of
|
2017-02-03 21:29:24 +01:00
|
|
|
progress. If you speak a language other than English, your help with
|
2016-04-29 06:55:09 +02:00
|
|
|
translating Zulip would be greatly appreciated!
|
|
|
|
|
2016-10-19 14:50:23 +02:00
|
|
|
If you're interested in contributing translations to Zulip, please
|
2017-02-13 01:55:54 +01:00
|
|
|
join [#translation](https://chat.zulip.org/#narrow/stream/translation)
|
|
|
|
in the [Zulip development community server](chat-zulip-org.html), and
|
|
|
|
say hello. And please join the
|
|
|
|
[Zulip project on Transifex](https://www.transifex.com/zulip/zulip/)
|
|
|
|
and ask to join any languages you'd like to contribute to (or add new
|
|
|
|
ones). Transifex's notification system sometimes fails to notify the
|
|
|
|
maintainers when you ask to join a project, so please send a quick
|
|
|
|
email to zulip-core@googlegroups.com when you request to join the
|
|
|
|
project or add a language so that we can be sure to accept your
|
|
|
|
request to contribute.
|
2016-05-06 13:22:37 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
Zulip has full support for Unicode, so you can already use your
|
|
|
|
preferred language everywhere in Zulip.
|
|
|
|
|
2016-11-30 04:34:32 +01:00
|
|
|
## Translation style guides
|
|
|
|
|
|
|
|
We are building a collection of translation style guides for Zulip,
|
|
|
|
giving guidance on how Zulip should be translated into specific
|
|
|
|
languages (e.g. what word to translate words like "home" to):
|
|
|
|
|
2016-12-04 08:52:28 +01:00
|
|
|
* [Chinese](chinese.html)
|
2017-01-25 16:29:19 +01:00
|
|
|
* [German](german.html)
|
2016-11-27 16:38:57 +01:00
|
|
|
* [Polish](polish.html)
|
2017-01-03 16:52:42 +01:00
|
|
|
* [Russian](russian.html)
|
2016-11-30 04:34:32 +01:00
|
|
|
* [Spanish](spanish.html)
|
|
|
|
|
2017-03-03 12:42:07 +01:00
|
|
|
A great first step when getting started translating Zulip into a new
|
|
|
|
language is to write a style guide, since it greatly increases the
|
|
|
|
ability of future translators to translate in a way that's consistent
|
|
|
|
with what your work.
|
|
|
|
|
|
|
|
### Capitalization
|
|
|
|
|
|
|
|
We expect that all the English translatable strings in Zulip are
|
|
|
|
properly capitalized in a way consistent with how Zulip does
|
|
|
|
capitalization in general. This means that:
|
|
|
|
|
|
|
|
* The first letter of a sentence or phrase should be capitalized.
|
|
|
|
- Correct: "Manage streams"
|
|
|
|
- Incorrect: "Manage Streams"
|
|
|
|
* All proper nouns should be capitalized.
|
|
|
|
- Correct: "This is Zulip"
|
|
|
|
- Incorrect: "This is zulip"
|
|
|
|
* All common words like URL, HTTP, etc. should be written in their
|
|
|
|
standard forms.
|
|
|
|
- Correct: "URL"
|
|
|
|
- Incorrect: "Url"
|
|
|
|
|
|
|
|
We have a tool to check for the correct capitalization of the
|
|
|
|
translatable strings; this tool will not allow the Travis builds to
|
|
|
|
pass in case of errors. You can use our capitalization checker to
|
2017-06-11 19:12:54 +02:00
|
|
|
validate your code by running `./tools/check-capitalization`. If you
|
2017-03-03 12:42:07 +01:00
|
|
|
think that you have a case where our capitalization checker tool
|
|
|
|
wrongly categorizes a string as not capitalized, you can add an
|
|
|
|
exception in the `tools.lib.capitalization.IGNORED_PHRASES` list to
|
|
|
|
make the tool pass.
|
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
Please, stick to these while translating, and feel free to point out
|
2017-03-03 12:42:07 +01:00
|
|
|
any strings that should be improved or fixed.
|
2016-06-30 13:56:04 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
## Translation process
|
2016-06-30 13:56:04 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
The end-to-end process to get the translations working is as follows.
|
2016-06-30 13:56:04 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
Please note that you don't need to do this if you're translating; this is
|
|
|
|
only to describe how the whole process is. If you're interested in
|
|
|
|
translating, you should check out the
|
|
|
|
[translators' workflow](#translators-workflow).
|
2016-06-30 13:56:04 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
1. The strings are marked for translation (see sections for
|
2016-10-19 14:51:20 +02:00
|
|
|
[backend](#backend-translations) and
|
|
|
|
[frontend](#frontend-translations) translations for details on
|
|
|
|
this).
|
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
2. Translation [resource][] files are created using the `./manage.py
|
2016-10-20 00:32:28 +02:00
|
|
|
makemessages` command. This command will create, for each language,
|
2016-10-20 00:34:31 +02:00
|
|
|
a resource file called `translations.json` for the frontend strings
|
|
|
|
and `django.po` for the backend strings.
|
2016-06-30 13:56:04 +02:00
|
|
|
|
|
|
|
The `makemessages` command is idempotent in that:
|
2016-06-27 10:32:15 +02:00
|
|
|
|
2016-06-30 13:56:04 +02:00
|
|
|
- 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.
|
2016-05-06 13:22:37 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
3. Those resource files are uploaded to Transifex by a maintainer using the
|
2016-10-20 00:34:31 +02:00
|
|
|
`tx push -s -a` command.
|
2016-06-30 13:56:04 +02:00
|
|
|
|
2016-10-20 00:34:31 +02:00
|
|
|
4. Translators translate the strings in Transifex.
|
|
|
|
|
2017-02-12 01:57:50 +01:00
|
|
|
5. The translations are downloaded back into the codebase by a
|
|
|
|
maintainer, using `tx pull -a -f --mode=developer` (since we don't
|
|
|
|
use the Transifex review flow).
|
2017-02-03 21:29:24 +01:00
|
|
|
|
|
|
|
## Translators' workflow
|
|
|
|
|
|
|
|
These are the steps you should follow if you want to help to translate
|
|
|
|
Zulip:
|
|
|
|
|
|
|
|
1. Join us on Zulip and ask for access to the organization, as
|
|
|
|
[described](#translating-zulip) at the beginning.
|
|
|
|
|
|
|
|
2. Make sure you have access to Zulip's dashboard in Transifex.
|
|
|
|
|
|
|
|
3. Ask a maintainer to update the strings.
|
|
|
|
|
|
|
|
4. Translate the strings for your language in Transifex.
|
|
|
|
|
|
|
|
Some useful tips for your translating journey:
|
|
|
|
|
2017-02-03 23:45:00 +01:00
|
|
|
- Follow your language's [translation guide](#translation-style-guides).
|
|
|
|
Keeping it open in a tab while translating is very handy. If one
|
|
|
|
doesn't exist one, write one as you go; they're easiest to write as
|
|
|
|
you go along and will help any future translators a lot.
|
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
- Don't translate variables or code (usually preceded by a `%`, or inside
|
|
|
|
HTML tags `<...>`).
|
|
|
|
|
2017-02-13 01:55:54 +01:00
|
|
|
- When in doubt, ask for context in
|
|
|
|
[#translation](https://chat.zulip.org/#narrow/stream/translation) in
|
|
|
|
the [Zulip development community server](chat-zulip-org.html).
|
2017-02-03 21:29:24 +01:00
|
|
|
|
|
|
|
- If there are multiple possible translations for a term, search for it in
|
|
|
|
the *Concordance* tool (the button with a magnet in the top right corner).
|
|
|
|
|
|
|
|
It will show if anyone translated that term before, so we can achieve good
|
|
|
|
consistency with all the translations, no matter who makes them.
|
|
|
|
|
|
|
|
- Pay attention to capital letters and punctuation. Details make the
|
|
|
|
difference!
|
|
|
|
|
|
|
|
- Take advantage of the hotkeys the Transifex Web Editor provides, such as
|
|
|
|
`Tab` for saving and going to the next string.
|
|
|
|
|
|
|
|
## Testing translations
|
|
|
|
|
2017-02-03 23:45:00 +01:00
|
|
|
This section assumes you have a
|
|
|
|
[Zulip development environment](readme-symlink.html#installing-the-zulip-development-environment)
|
|
|
|
setup.
|
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
First of all, download the updated resource files from Transifex using the
|
2017-02-12 01:57:50 +01:00
|
|
|
`tx pull -a --mode=developer` command (it will require some
|
2017-02-03 21:29:24 +01:00
|
|
|
[initial setup](#transifex-cli-setup)). This command will download the
|
|
|
|
resource files from Transifex and replace your local resource files with
|
|
|
|
them.
|
|
|
|
|
|
|
|
Then, make sure that you have compiled the translation strings using
|
|
|
|
`./manage.py compilemessages`.
|
|
|
|
|
|
|
|
Django figures out the effective language by going through the following
|
|
|
|
steps:
|
|
|
|
|
2017-02-03 23:45:00 +01:00
|
|
|
1. It looks for the language code in the url (e.g. `/de/`).
|
2017-02-03 21:29:24 +01:00
|
|
|
2. It looks for the `LANGUAGE_SESSION_KEY` key in the current user's
|
|
|
|
session.
|
|
|
|
3. It looks for the cookie named 'django_language'. You can set a
|
|
|
|
different name through the `LANGUAGE_COOKIE_NAME` setting.
|
|
|
|
4. It looks for the `Accept-Language` HTTP header in the HTTP request.
|
|
|
|
Normally your browser will take care of this.
|
|
|
|
|
|
|
|
The easiest way to test translations is through the i18n URLs, e.g., if you
|
|
|
|
have German translations available, you can access the German version of a
|
|
|
|
page by going to `/de/path_to_page` in your browser.
|
|
|
|
|
|
|
|
To test translations using other methods you will need an HTTP client
|
|
|
|
library like `requests`, `cURL` or `urllib`. Here is some sample code to
|
|
|
|
test `Accept-Language` header using Python and `requests`:
|
|
|
|
|
|
|
|
```
|
|
|
|
import requests
|
|
|
|
headers = {"Accept-Language": "de"}
|
|
|
|
response = requests.get("http://localhost:9991/login/", headers=headers)
|
|
|
|
print(response.content)
|
|
|
|
```
|
|
|
|
|
|
|
|
## Setting the default language in Zulip
|
|
|
|
|
|
|
|
Zulip allows you to set the default language through the settings
|
|
|
|
page, in the 'Display settings' section. The URL will be
|
|
|
|
`/#settings/display-settings` on your realm.
|
|
|
|
|
2017-02-03 23:45:00 +01:00
|
|
|
Organizations can set the default language for new users in their
|
2017-04-07 21:39:58 +02:00
|
|
|
organization on the `/#organization` page.
|
2017-02-03 23:45:00 +01:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
## Translation resource files
|
|
|
|
|
|
|
|
All the translation magic happens through resource files which hold
|
|
|
|
the translated text. Backend resource files are located at
|
|
|
|
`static/locale/<lang_code>/LC_MESSAGES/django.po`, while frontend
|
|
|
|
resource files are located at
|
|
|
|
`static/locale/<lang_code>/translations.json`.
|
2016-10-20 00:34:31 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
These files are uploaded to [Transifex][], where they can be translated.
|
2016-06-30 13:56:04 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
## Backend translations
|
2016-06-30 13:56:04 +02:00
|
|
|
|
|
|
|
All user-facing text in the Zulip UI should be generated by an HTML
|
2017-06-22 01:29:23 +02:00
|
|
|
template so that it can be translated. For text generated in the
|
|
|
|
backend, including logged-out ("portico") pages and the webapp's base
|
|
|
|
content, we use the [Jinja2][] template engine.
|
2016-05-06 13:22:37 +02:00
|
|
|
|
2017-06-22 01:29:23 +02:00
|
|
|
To mark a string for translation in a Jinja2 template, you
|
2017-06-15 20:47:00 +02:00
|
|
|
can use the `_()` function in the templates like this:
|
2016-05-06 13:22:37 +02:00
|
|
|
|
|
|
|
```
|
|
|
|
{{ _("English text") }}
|
|
|
|
```
|
|
|
|
|
2017-06-22 01:29:23 +02:00
|
|
|
If a piece of text contains both a literal string component and variables,
|
2016-05-06 13:22:37 +02:00
|
|
|
you can use a block translation, which makes use of placeholders to
|
2016-06-30 13:56:04 +02:00
|
|
|
help translators to translate an entire sentence. To translate a
|
2017-06-15 20:47:00 +02:00
|
|
|
block, Jinja2 uses the [trans][] tag. So rather than writing
|
|
|
|
something ugly and confusing for translators like this:
|
2016-05-06 13:22:37 +02:00
|
|
|
|
|
|
|
```
|
|
|
|
# Don't do this!
|
|
|
|
{{ _("This string will have") }} {{ value }} {{ _("inside") }}
|
|
|
|
```
|
|
|
|
|
|
|
|
You can instead use:
|
|
|
|
|
|
|
|
```
|
|
|
|
{% trans %}This string will have {{ value }} inside.{% endtrans %}
|
|
|
|
```
|
|
|
|
|
2017-06-11 00:48:49 +02:00
|
|
|
A string in Python can be marked for translation using the `_()` function,
|
|
|
|
which can be imported as follows:
|
|
|
|
|
|
|
|
```
|
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
```
|
|
|
|
|
2016-06-30 13:56:04 +02:00
|
|
|
Zulip expects all the error messages to be translatable as well. To
|
2016-10-19 14:51:20 +02:00
|
|
|
ensure this, the error message passed to `json_error` and
|
|
|
|
`JsonableError` should always be a literal string enclosed by `_()`
|
|
|
|
function, e.g.:
|
2016-06-01 13:20:45 +02:00
|
|
|
|
|
|
|
```
|
|
|
|
json_error(_('English Text'))
|
|
|
|
JsonableError(_('English Text'))
|
|
|
|
```
|
|
|
|
|
|
|
|
To ensure we always internationalize our JSON errors messages, the
|
2017-04-21 23:07:06 +02:00
|
|
|
Zulip linter (`tools/lint`) checks for correct usage.
|
2016-06-01 13:20:45 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
## Frontend translations
|
2016-05-13 12:44:03 +02:00
|
|
|
|
2017-06-22 01:29:23 +02:00
|
|
|
For text generated in the frontend, live-rendering HTML from
|
|
|
|
JavaScript for things like the main message feed, we use the
|
|
|
|
[Handlebars][] template engine and sometimes work directly from
|
|
|
|
JavaScript code. In both cases, we use the [i18next][] library
|
|
|
|
for translations.
|
|
|
|
|
|
|
|
To mark a string translatable in JavaScript files, pass it to the
|
|
|
|
`i18n.t` function.
|
2016-06-30 13:56:04 +02:00
|
|
|
|
|
|
|
```
|
|
|
|
i18n.t('English Text', context);
|
|
|
|
```
|
|
|
|
|
2017-07-01 02:23:16 +02:00
|
|
|
Variables in a translated frontend string are enclosed in
|
|
|
|
double-underscores, like `__variable__`:
|
|
|
|
|
|
|
|
```
|
|
|
|
i18n.t('English text with a __variable__', {'variable': 'Variable value'});
|
|
|
|
```
|
2016-06-30 13:56:04 +02:00
|
|
|
|
|
|
|
`i18next` also supports plural translations. To support plurals make
|
2017-01-15 05:13:22 +01:00
|
|
|
sure your resource file contains the related keys:
|
2016-06-30 13:56:04 +02:00
|
|
|
|
|
|
|
```
|
|
|
|
{
|
|
|
|
"en": {
|
|
|
|
"translation": {
|
|
|
|
"key": "item",
|
|
|
|
"key_plural": "items",
|
|
|
|
"keyWithCount": "__count__ item",
|
|
|
|
"keyWithCount_plural": "__count__ items"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
With this resource you can show plurals like this:
|
|
|
|
|
|
|
|
```
|
|
|
|
i18n.t('key', {count: 0}); // output: 'items'
|
|
|
|
i18n.t('key', {count: 1}); // output: 'item'
|
|
|
|
i18n.t('key', {count: 5}); // output: 'items'
|
|
|
|
i18n.t('key', {count: 100}); // output: 'items'
|
|
|
|
i18n.t('keyWithCount', {count: 0}); // output: '0 items'
|
|
|
|
i18n.t('keyWithCount', {count: 1}); // output: '1 item'
|
|
|
|
i18n.t('keyWithCount', {count: 5}); // output: '5 items'
|
|
|
|
i18n.t('keyWithCount', {count: 100}); // output: '100 items'
|
|
|
|
```
|
|
|
|
|
|
|
|
For further reading on plurals, read the [official] documentation.
|
|
|
|
|
2017-08-23 09:15:40 +02:00
|
|
|
By default, all text is escaped by i18next. To unescape a text you can use
|
|
|
|
double-underscores followed by a dash `__-` like this:
|
|
|
|
|
|
|
|
```
|
|
|
|
i18n.t('English text with a __- variable__', {'variable': 'Variable value'});
|
|
|
|
```
|
|
|
|
|
|
|
|
For more information, you can read the official [unescape] documentation.
|
|
|
|
|
2017-07-01 02:23:16 +02:00
|
|
|
### Handlebars templates
|
|
|
|
|
|
|
|
For translations in Handlebars templates we also use `i18n.t`, through two
|
|
|
|
Handlebars [helpers][] that Zulip registers. The syntax for simple strings is:
|
2016-06-30 13:56:04 +02:00
|
|
|
|
|
|
|
```
|
|
|
|
{{t 'English Text' }}
|
|
|
|
```
|
|
|
|
|
|
|
|
The syntax for block strings or strings containing variables is:
|
|
|
|
|
|
|
|
```
|
2016-12-15 10:10:19 +01:00
|
|
|
{{#tr context}}
|
2016-06-30 13:56:04 +02:00
|
|
|
Block of English text.
|
|
|
|
{{/tr}}
|
|
|
|
|
|
|
|
var context = {'variable': 'variable value'};
|
2016-12-15 10:10:19 +01:00
|
|
|
{{#tr context}}
|
2016-06-30 13:56:04 +02:00
|
|
|
Block of English text with a __variable__.
|
|
|
|
{{/tr}}
|
|
|
|
```
|
2016-05-13 12:44:03 +02:00
|
|
|
|
2017-07-01 02:23:16 +02:00
|
|
|
Just like in JavaScript code, variables are enclosed in double
|
|
|
|
underscores `__`.
|
|
|
|
|
|
|
|
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
|
|
|
|
processed by `i18n.t`, so that the string to be translated wouldn't be
|
|
|
|
constant. We have a linter to enforce that translated blocks don't
|
|
|
|
contain handlebars.
|
|
|
|
|
2016-06-30 13:56:04 +02:00
|
|
|
The rules for plurals are same as for JavaScript files. You just have
|
2016-10-19 14:51:20 +02:00
|
|
|
to declare the appropriate keys in the resource file and then include
|
|
|
|
the `count` in the context.
|
2016-05-13 12:44:03 +02:00
|
|
|
|
2017-06-30 07:07:49 +02:00
|
|
|
|
2017-02-03 21:29:24 +01:00
|
|
|
## Transifex config
|
|
|
|
|
|
|
|
The config file that maps the resources from Zulip to Transifex is
|
2017-02-03 23:45:00 +01:00
|
|
|
located at `.tx/config`.
|
2017-02-03 21:29:24 +01:00
|
|
|
|
2017-02-12 01:57:50 +01:00
|
|
|
## Transifex CLI setup
|
2017-02-03 21:29:24 +01:00
|
|
|
|
|
|
|
In order to be able to run `tx pull` (and `tx push` as well, if you're a
|
|
|
|
maintainer), you have to specify your Transifex credentials in a config
|
|
|
|
file, located at `~/.transifexrc`.
|
|
|
|
|
|
|
|
You can find details on how to set it up [here][transifexrc], but it should
|
|
|
|
look similar to this (with your credentials):
|
|
|
|
|
|
|
|
```
|
|
|
|
[https://www.transifex.com]
|
|
|
|
username = user
|
|
|
|
token =
|
|
|
|
password = p@ssw0rd
|
|
|
|
hostname = https://www.transifex.com
|
|
|
|
```
|
|
|
|
|
|
|
|
This basically identifies you as a Transifex user, so you can access your
|
|
|
|
organizations from the command line.
|
|
|
|
|
2016-05-13 12:44:03 +02:00
|
|
|
|
2016-05-06 13:22:37 +02:00
|
|
|
[Jinja2]: http://jinja.pocoo.org/
|
|
|
|
[Handlebars]: http://handlebarsjs.com/
|
|
|
|
[trans]: http://jinja.pocoo.org/docs/dev/templates/#i18n
|
2016-06-30 13:56:04 +02:00
|
|
|
[i18next]: http://i18next.com
|
|
|
|
[official]: http://i18next.com/translate/pluralSimple/
|
2017-08-23 09:15:40 +02:00
|
|
|
[unescape]: https://www.i18next.com/interpolation.html#unescape
|
2016-06-30 13:56:04 +02:00
|
|
|
[helpers]: http://handlebarsjs.com/block_helpers.html
|
|
|
|
[resource]: http://i18next.com/translate/
|
2017-02-03 21:29:24 +01:00
|
|
|
[Transifex]: https://transifex.com
|
|
|
|
[transifexrc]: https://docs.transifex.com/client/client-configuration#transifexrc
|