We’re using gettext for translations. All the actual translations are carried out via Pontoon.
Some commands wrap standard gettext tools. To run these commands you’ll need to ensure you have done the following steps:
npm install to install all the project dependencies.$PATH by checking the output of which gettext.Locales are defined in two parts.
Entirely new locales are added via src/amo/languages.js.
Enabled locales are defined separately. See config/default.js and look for the langs list.
These lists should always be kept in sync with addons-server. See the documentation here for details.
Add the new language to the list and then run:
# create the locale for a newly added language.
NODE_PATH='./:./src' bin/create-locales
Locales are updated automatically as a part of our CI. On every push to master npm run extract-locales is run which extracts locale strings from our codebase, merges any changes to the source language files and commits the changes.
You can run this command manually on your local environment any time to check the output strings.
Github actions internally prevent infinite loops by default.
This command creates the JSON files which are then built into JS bundles by webpack when the build step is run. This happens automatically as part of the deployment process.
Since dist files are created when needed you only need to build and commit the JSON to the repo:
bin/build-locales
To set up a component to be translated there are two pieces of code to know about.
We use Jed as the API for providing gettext functions inside React components. An initialized Jed instance has all the gettext related functionality exposed as methods. There is a fancy chained API but we’ve stuck to a more traditional approach.
Before we get into how to make use of these functions let’s take a look at how the Jed instance is exposed to our components.
The translation provider is used to pass down a Jed instance via context to components lower down in the component hierarchy. This part is already done for you in addons-frontend. So you should only need to worry about wrapping your components as detailed in the next section.
The translate Higher Order Component is a helper that wraps any component and takes the Jed i18n instance from context and makes it available in the wrapped component’s props.
Here’s an example of a basic component setup for translation:
import * as React from 'react';
import PropTypes from 'prop-types';
import translate from 'core/i18n/translate';
export class MyTranslatedComponent extends React.Component {
static propTypes = {
i18n: PropTypes.object.isRequired,
};
render() {
const { i18n } = this.props;
return (
<div>
<p>{i18n.gettext('Something translated')}</p>
</div>
);
}
}
export default translate()(MyTranslatedComponent);
That’s pretty much all there is to it.
Once you have i18n available to your component you can then use any of the Jed methods exposed on the i18n object.
gettext = function ( key )
dgettext = function ( domain, key )
dcgettext = function ( domain, key, category )
ngettext = function ( singular_key, plural_key, value )
dngettext = function ( domain, singular_ley, plural_key, value )
dcngettext = function ( domain, singular_key, plural_key, value, category )
pgettext = function ( context, key )
dpgettext = function ( domain, context, key )
npgettext = function ( context, singular_key, plural_key, value )
dnpgettext = function ( domain, context, singular_key, plural_key, value )
dcnpgettext = function ( domain, context, singular_key, plural_key, value, category )
sprintf = function ( string, substitutions)
sprintfAs you can see a sprintf function is also available. You can use this to provide substitutions in gettext wrapped strings.
There are two flavours to this, numbered placeholders or named ones.
Here’s the numbered approach:
i18n.sprintf(i18n.gettext('I like your %1$s %2$s.'), 'red', 'shirt'));
and here’s the named arg approach:
i18n.sprintf(i18n.gettext('I like your %(colour)s %(garment)s.'), { colour: 'red', garment: 'shirt' }));
Both of these approaches allow for translators to re-order the substitution vars.
Generally we’re looking to avoid having HTML in the middle of translation strings as much as possible.
If you need HTML it’s better to use substitutions to add the HTML than leave HTML in the translation. Take the following string as an example:
i18n.gettext('Take a look at the <a href="#">documentation</a>');
Using sprintf we can provide use start and end substitutions. This way there’s no HTML in the extracted string.
i18n.sprintf(
i18n.gettext('Take a look at the %(start_link)sdocumentation%(end_link)s'),
{ start_link: '<a href="#">', end_link: '</a>' },
);
You can also use DOMPurify to sanitize strings that may contain HTML following substitutions so that anything not explicitly allowed is removed. DOMPurify will also help protect against malformed HTML in case opening and closing tag substitutions vars get swapped around inadvertently.