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.Locale files are stored in an external repository that you need to clone into the locale/ directory inside your addons-frontend clone in order to build the files needed for those locales to work.
git clone https://github.com/mozilla-l10n/addons-frontend-l10n.git locale/
You should also update that clone whenever you want to test fresh translations locally. CI automatically pulls the latest version when building a new image for dev/stage/prod environments and when running tests.
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, commits the changes to a branch on https://github.com/mozilla-l10n/addons-frontend-l10n and creates a pull request from that branch.
That PR can then be reviewed and approved by a manager from the l10n team, and Pontoon automatically imports the resulting .po files and commits back to that dedicated l10n repo.
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.