diff --git a/docs/subsystems/html-css.md b/docs/subsystems/html-css.md
index fb1b7d4f05..032d680299 100644
--- a/docs/subsystems/html-css.md
+++ b/docs/subsystems/html-css.md
@@ -277,8 +277,6 @@ function in those scenarios, add it to `zulip_test`. This is also
[Jinja2]: http://jinja.pocoo.org/
[Handlebars]: https://handlebarsjs.com/
[trans]: http://jinja.pocoo.org/docs/dev/templates/#i18n
-[i18next]: https://www.i18next.com
-[official]: https://www.i18next.com/plurals.html
[jconditionals]: http://jinja.pocoo.org/docs/2.9/templates/#list-of-control-structures
[hconditionals]: https://handlebarsjs.com/guide/#block_helpers.html
[translation]: ../translating/translating.md
diff --git a/docs/translating/internationalization.md b/docs/translating/internationalization.md
index 00d1a361b6..0d1a65ee52 100644
--- a/docs/translating/internationalization.md
+++ b/docs/translating/internationalization.md
@@ -77,7 +77,7 @@ The end-to-end tooling process for translations in Zulip is as follows.
[frontend](#frontend-translations) translations for details on
this).
-2. Translation [resource][] files are created using the `./manage.py
+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.
@@ -208,73 +208,46 @@ Zulip linter (`tools/lint`) attempts to verify correct usage.
## Frontend translations
-We use the [i18next][] library for frontend translations when dealing
+We use the [FormatJS][] library for frontend translations when dealing
with [Handlebars][] templates or JavaScript.
To mark a string translatable in JavaScript files, pass it to the
-`i18n.t` function.
+`intl.formatMessage` function, which we alias to `$t` in `intl.js`:
-```
-i18n.t('English Text', context);
+```js
+$t({defaultMessage: "English Text"})
```
-Variables in a translated frontend string are enclosed in
-double-underscores, like `__variable__`:
+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:
-```
-i18n.t('English text with a __variable__', {'variable': 'Variable value'});
+```js
+$t({defaultMessage: "English text with a {variable}"}, {variable: "Variable value"})
```
-`i18next` also supports plural translations. To support plurals make
-sure your resource file contains the related keys:
+FormatJS uses the standard [ICU MessageFormat][], which includes
+useful features such as plural translations.
+`$t` does not escape any variables, so if your translated string is
+eventually going to be used as HTML, use `$t_html` instead.
+
+```js
+$("#foo").html(
+ $t_html({defaultMessage: "HTML with a {variable}"}, {variable: "Variable value"})
+);
```
-{
- "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.
-
-By default, all text is escaped by i18next. To unescape a text you can use
-double-underscores followed by a dash and space `__- ` like this:
-
-```
-i18n.t('English text with a __- variable__', {'variable': 'Variable value'});
-```
-
-For more information, you can read the official [unescape] documentation.
### Handlebars templates
-For translations in Handlebars templates we also use `i18n.t`, through two
+For translations in Handlebars templates we also use FormatJS, through two
Handlebars [helpers][] that Zulip registers. The syntax for simple strings is:
```
{{t 'English Text' }}
```
-If you are passing a translated string to a Handlebars Partial, you can use:
+If you are passing a translated string to a Handlebars partial, you can use:
```
{{> template_name
@@ -291,25 +264,23 @@ The syntax for block strings or strings containing variables is:
var context = {'variable': 'variable value'};
{{#tr context}}
- Block of English text with a __variable__.
+ Block of English text with a {variable}.
{{/tr}}
```
-Just like in JavaScript code, variables are enclosed in double
-underscores `__`.
+Just like in JavaScript code, variables are enclosed in *single*
+braces (rather than the usual Handlebars double braces). Unlike in
+JavaScript code, variables are automatically escaped by our Handlebars
+helper.
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
+processed by FormatJS, so that the string to be translated wouldn't be
constant. We have a linter to enforce that translated blocks don't
contain handlebars.
-The rules for plurals are same as for JavaScript files. You just have
-to declare the appropriate keys in the resource file and then include
-the `count` in the context.
-
## Transifex config
The config file that maps the resources from Zulip to Transifex is
@@ -339,11 +310,9 @@ organizations from the command line.
[Jinja2]: http://jinja.pocoo.org/
[Handlebars]: https://handlebarsjs.com/
[trans]: http://jinja.pocoo.org/docs/dev/templates/#i18n
-[i18next]: https://www.i18next.com
-[official]: https://www.i18next.com/plurals.html
-[unescape]: https://www.i18next.com/interpolation.html#unescape
+[FormatJS]: https://formatjs.io/
+[ICU MessageFormat]: https://formatjs.io/docs/intl-messageformat
[helpers]: https://handlebarsjs.com/guide/block-helpers.html
-[resource]: https://www.i18next.com/add-or-load-translations.html
[Transifex]: https://transifex.com
[transifexrc]: https://docs.transifex.com/client/client-configuration#transifexrc
[html-templates]: ../subsystems/html-css.html#html-templates
diff --git a/docs/tutorials/writing-views.md b/docs/tutorials/writing-views.md
index 2c99efbcba..604e7fd541 100644
--- a/docs/tutorials/writing-views.md
+++ b/docs/tutorials/writing-views.md
@@ -291,7 +291,7 @@ channel.patch({
data: data,
success: function (response_data) {
if (response_data.name !== undefined) {
- ui_report.success(i18n.t("Name changed!"), name_status);
+ ui_report.success($t({defaultMessage: "Name changed!"}), name_status);
}
...
```
diff --git a/frontend_tests/node_tests/i18n.js b/frontend_tests/node_tests/i18n.js
index 5462655c1f..4ef65d7292 100644
--- a/frontend_tests/node_tests/i18n.js
+++ b/frontend_tests/node_tests/i18n.js
@@ -20,9 +20,8 @@ page_params.translation_data = {
// All of our other tests stub out i18n activity;
// here we do a quick sanity check on the engine itself.
-// We use `i18n.js` to initialize `i18next` and
-// to set `i18n` to `i18next` on the global namespace
-// for `templates.js`.
+// `i18n.js` initializes FormatJS and is imported by
+// `templates.js`.
unmock_module("../../static/js/i18n");
const {$t, $t_html} = zrequire("i18n");
zrequire("templates");