title |
---|
Internationalization |
We are an application with lots of users all over the world. To help them use Metabase in their own language, we mark all of our strings as i18n.
If you need to add new strings (try to be judicious about adding copy) do the following:
- Tag strings in the frontend using
t
andjt
ES6 template literals (see more details in https://ttag.js.org/):
const someString = t`Hello ${name}!`;
const someJSX = <div>{jt`Hello ${name}`}</div>;
and in the backend using trs
(to use the site language) or tru
(to use the current User's language):
(trs "Hello {0}!" name)
If you see incorrect or missing strings for your language, please visit our POEditor project and submit your fixes there.
Metabase allows for translations into many languages. An authoritative list can be found in resources/locales.clj
.
Metabase is concerned about localization into two distinct locales: translating into the server's locale and translating into the user's locale. The distinction is largely: will this be logged on the server or sent over the wire back to the user.
To translate a string for the server, use metabase.util.i18n/trs
and for the user's locale, use the similar metabase.util.i18n/tru
. Think tr-server
and tr-user
.
At a high level, the string to be translated is treated as a lookup key into a map of source-string -> localized-string. This translated string is used like so:
;; from source of `translate` in `metabase.util.i18n`
(.format (MessageFormat. looked-up-string) (to-array args))
Everything else is largely bookkeeping. This uses the java.text.MessageFormat class for splicing in format args.
The functions trs
and tru
create instances of two records, SiteLocalizedString
and UserLocalizedString
respectively with overrides to the toString
method. This method will do the lookup to the current locale (user or site as appropriate), lookup the string to be translated to the associated translated string, and then call the .format
method on the MessageFormat
.
One step in our build process creates an edn file of source to translated string for each locale we support. These are located in resources/i18n
. If you do not have these files, you can run bin/build-translation-resources
to generate them.
We have lots of contributors who help us keep a corpus of translated strings into many different languages. We use POEditor to keep an authoritative list. We export .po
files from this, which is essentially a dictionary from source to translated string. As part of our build process we format these files as edn files, maps from the source to translated string, for each locale.
Besides string literals, we also want to translate strings that have arguments spliced into the middle. We use the syntax from the java.text.MessageFormat class mentioned before. These are zero-indexed args of the form {0}
, {1}
.
eg,
(trs "{0} accepted their {1} invite" (:common_name new-user) (app-name-trs))
(tru "{0}th percentile of {1}" p (aggregation-arg-display-name inner-query arg))
(tru "{0} driver does not support foreign keys." driver/*driver*)
Every string language needs an escape character. Since {0}
is an argument to be spliced in, how would you put a literal "{0}" in the string. The apostrophe serves this role and is described in the MessageFormat javadocs.
These is an unfortunate side effect of this though. Since the apostrophe is such a commeon part of speech (especially in french), we often can end up with escape characters used as a regular part of a string rather than the escape character. Format strings need to use double apostrophes like (deferred-tru "SAML attribute for the user''s email address")
to escape the apostrophe.
There are lots of translated strings in French that use a single apostrophe incorrectly. (eg "l'URL" instead of "l''URL"). We have a manual fix to this in bin/i18n/src/i18n/create_artifacts/backend.clj
where we try to identify these apostrophes which are not escape characters and replace them with a double quote.