Skip to main content

Current as of November 6, 2023

Translations for Mozilla accounts and Subscription Platform are located in the fxa-content-server-l10n repo. This includes translations for UI and emails, but does not include legal documents.

Process overview

  1. Adding new strings for localization
    • Use legacy gettext for fxa-content-server
    • Use Fluent everywhere else
  2. Extraction: Strings are extracted to the l10n repo
  3. Localization: Strings are localized on Pontoon
  4. Build: Localized strings are copied from the l10n repo and bundled with builds

Adding new strings for localization

With gettext

Within fxa-content-server, all localizable strings are wrapped in a t method. t is a signal to the l10n extraction script that the embedded string is meant for translation. Within mustache templates, strings meant for extraction are wrapped in {{#t}}{{/t}} or {{#unsafeTranslate}}{{/unsafeTranslate}} tags.

Strings wrapped in a {{#t}}{{/t}} tag are HTML escaped by default to prevent unexpected HTML from being written to the DOM. HTML can be written to the DOM from a string that is translated using the {{#unsafeTranslate}}{{/unsafeTranslate}} tag. Within an unsafeTranslate tag, named variables must contain the escaped prefix to remind developers that the variable must be HTML escaped before being written.

For example:

{{#unsafeTranslate}}Enter the code that was sent to %(escapedEmail)s within 5 minutes.{{/unsafeTranslate}}

When translated by the content-server, escapedEmail will be passed as a variable to the translate function, causing %(escapedEmail)s to be replaced with the passed in value.

Gettext comments can be used to provide extra context to translators to allow them to effectively translate strings that could be vague in different languages. Text preceeded by /// is extracted as a gettext comment (#.), while adding msgctxt marks the text as a separate entry for localization even if there are other strings with identical text. For example:

/// submit button
const buttonSignInText = this.translate(t('Sign in'), {
msgctxt: 'submit button',
});
#. submit button
msgctxt "submit button"
msgid "Sign in"
msgstr "Identificarse"

See gettext documentation for more details.

With Fluent

note

Developers should read the Fluent syntax guide before adding or modifying Fluent strings. Particular care should be taken when including variables, attributes or embedded elements in localized strings.

To localize an element with Fluent, the message must be added to an FTL file with a unique identifier. In the Fxa repo, Fluent strings are kept close to the file where they are used by adding an en.ftl file at the component-level. These files are then concatenated at the package-level with a merge-ftl grunttask. For example, all en.ftl files in fxa-settings are concatenanted into settings.ftl before the strings are extracted to the l10n repo.

A very basic example of an en.ftl file:

## Example component
## This is a group comment that applies to all strings until anoter group comment is added - only use it at the top of the file

# This is a standalone comments for developers, it won't show up on Pontoon
# The id, file name and project will be shown as context on Pontoon

example-component-heading = Page heading example
# This is a note that applies only to example-subheading
example-component-subheading = Page subheading example

Our convention for naming FTL id:

  1. FTL IDs should be all lower-case alphanumeric characters.
  2. Words should be separated with hyphens (kebab case).
  3. IDs should be prefixed with the name of the component/page (e.g., bento-menu-... for BentoMenu component files)
  4. If an existing string is modified, add a version suffix to the string id (e.g., bento-menu-heading-v2).
  5. The id should be descriptive for both the developer and the localizer, as it might be used as context to understand how the string is used.
  6. Prefer whole words vs abbreviations (e.g., 'button' instead of 'btn') as they are clearer for non-native English speakers.

React-based packages (fxa-payments, fxa-react, fxa-settings) use React bindings for Project Fluent.

Preferred implementation: use FtlMsg wrapper element and useFtlMsgResolver hook for new additions, or when updating existing components that directly use the Localized component and useLocalization hook. The custom FtlMsg wrapper and useFtlMsgResolver hook provide additional safety by forcing the inclusion of fallback text and preparing for additional l10n testing (see L10n/I18n Improvements ADR)

<FtlMsg id="example-component-heading">
<h1>Page heading example</h1>
</FtlMsg>

The useFtlMsgResolver can be used to pass a localized string as a prop to a child component or to add a localized string as an element attribute.

const ftlMsgResolver = useFtlMsgResolver();
const localizedMessage = ftlMsgResolver.getMsg('ftl-id','Fallback text');

Branding terms (such as Mozilla, Mozilla accounts) can be found in branding.ftl and are shared by all packages that use Fluent strings. The branding file is bundled with other package-level ftl files at build time into a main.ftl file to ensure the branding terms are made available for use as placeables in other strings.

Fluent strings used in emails are implemented a bit differently because of MJML template constraints. See fxa-auth-server email localization for details.

Modifying strings

If a string must be modified, a new string must be created (for example by adding -2 or -v2 to the previous id). Modifying an existing string won't trigger a localization request - the 'en' string would be updated, but localized strings would all have the old text as the localizers would not be notified of the change. If a string used as a placeable in other strings is updated, all dependant strings must also be bumped to a new version to include the updated id and will trigger a new localization request.

E.g.,

unique-id = Some string
another-unique-id = { unique-id } is repeated here
unique-id-v2 = An updated string
another-unique-id-v2 = { unique-id-v2 } is repeated here

A few important notes about localization with Fluent/React

Attributes

Attributes can be localized declaratively with <Localized> or <FtlMsg> components. See the Fluent Wiki for examples and notes on the importance of including the attrs prop. If attrs is not passed, no attributes will be set!

An alternative is to use a hook (either useLocalization or useFtlMsgResolver) to pass the localized string as a prop. In this scenario, the FTL id should include the name of the attribute (e.g., component-name-image-alt-text) to provide context for localizers.

Markup/embedded elements in strings

If the string contains markup (for example, a link), the elems prop MUST be used. See Fluent Wiki for examples and notes.

Using styling to uppercase/lowercase localized text

Just don't - locales may have different rules around capitalization. Best to apply capitalization to the default 'en' string with a comment about the context, and let localizers determine what is best for their locale.

See notes about paramtrization for brand terms.

Documentation about using Fluent:

Extraction process

Timed cron job on CircleCI runs twice weekely on Monday and Thursday afternoons. This job can also be run manually if needed (reach out to the Mozilla accounts l10n project manager).

  • Calls clone.sh to clone the l10n repo before extracting strings.
  • Runs the merge-ftl grunt tasks for required fxa packages.
  • Calls extract_strings.sh to extract strings from the fxa repo (including running l10n-extract for gettext) and create a commit with a random branch name.
  • Branch becomes a pull request on the fxa-content-server-l10n repository which must be merged after doing cursory checks to ensure new/changed strings “look right”. If l10n notices any issues that weren't spotted in the PR review, they will typically let the team know at this point so that a fix can be implemented before manually re-running the export script.
  • Once merged, Pontoon automatically pulls in string changes and notifies localizers.

Extraction process deep dive (gettext)

Gettext string extraction on the content server is done with a grunt task called l10n-extract. The content server is a mixture of JavaScript, TypeScript, JSX, Handlebars, and Mustache, and each of these file types may contain translatable strings. jsxextract is used to extract strings from a variety of file types, including JavaScript extensions such as babel-specific dynamic imports and class properties that jsx-gettext does not handle.

The translations in .po (Portable Object) files are stored with a relatively simple format. Here is an example:

#: .es5/views/confirm_reset_password.js:195
msgid "Password reset"
msgstr "Slaptažodžio atkūrimas"
  • First line (#: .es5…): location(s) where the string is used in the source code.
  • Second line (msgid): string in English.
  • Third line (msgstr): the translation (if available). If no translation is provided, msgstr is an empty string ""

Localization process

  • Localizers are notified that Mozilla accounts strings have changed.
  • Localizers make updates and a review request is issued.
  • Reviewer accepts changes.
  • Changes are pushed back to the fxa-content-server-l10n repo as a commit to master.
    • Every commit to master causes a linting job to be run.
      • The linter checks to ensure:
        • All HTML within the translations is well formed, e.g., no unclosed anchor tags.
        • No unexpected HTML elements, attributes or values exist.
        • All named variables in the translations exist in the English strings.
      • These checks prevent:
        • l10n being used as an attack vector since HTML element attributes can contain JavaScript or external links.
        • Incorrect page renderings due to malformed HTML in l10n strings.

Build process

The latest strings from fxa-content-server-l10n repo are cloned with each build (see decision outcome #4 of the the L10n/I18 Improvements ADR for details on this approach). Newly localized strings from Pontoon are not available (on stage/production) until a new release (including a dot release) is created.

  • On prebuild, l10n:prime pulls the latest localization files into target workspaces.
  • The build process calls a build-l10n task:
    • In fxa-content-server:
      • The l10n-create-json grunt task copies all translated .po files from the fxa-content-server-l10n repo and runs the grunt po2json task, once for each of the 2 .po files for each locale, resulting in 2 .json files for each locale.
      • A copy of the compiled JavaScript bundles is created for each locale.
      • Static JavaScript resources are deployed to a 3rd party CDN (Amazon) and Subresouce Integrity (SRI) is used to ensure the resources are not modified on the CDN. Each JavaScript bundle (per locale) has its own SRI hash.
    • In other localized packages (fxa-auth-server, fxa-payments, fxa-react, fxa-settings, fxa-shared):
      • Individual source en.ftl files for the default 'en' locale are merged into a package-level .ftl file (merge-ftl grunt task) to ensure local builds have access to the latest FTL strings.
      • l10n bundles are created for use by Fluent's <LocalizationProvider> with bundle.sh.

Additional notes

Dates and Times

Dates and times are a special case because dates and times are very locale specific. For example, in Europe it is common for July 10 to be written 10/07 whereas that would read October 10th to North Americans. All of our times and dates are translated using the locale-aware version of MomentJS. Since the locale-aware MomentJS is rather large (67kb), dates and times displayed in the Connected services of FxA are translated on the auth-server when requesting the devices and apps list.

Where possible in packages that use Fluent, we should instead use Fluent's DATETIME function.

Terms of Service and Privacy Policy are handled differently

The FxA Terms of Service and Privacy Policy are handled differently to other FxA translations. The most important difference is that these are considered legally binding documents, as such changes are driven by Mozilla’s legal team and source documents are held in the mozilla/legal-docs repository on Github. Any time either document is updated, paid specialist legal translators make the updates for other languages.

Unlike the rest of the FxA translations that are held in gettext .po files, the terms of service and privacy policy source documents are full markdown documents to allow the Legal team control over the formatting of the documents. During the FxA docker image build, the head of the legal-docs repo is pulled in and the markdown documents converted to HTML.

Text direction

In fxa-settings and fxa-payments, we use React-Helmet to set the text direction in the Head by comparing the rendered locale with a list of RTL languages. If the negotiated locale is a right-to-left (RTL) language, the page-content will be displayed in RTL layout.

note

Packages that use Tailwind for styling can use Tailwind's logical properties to automatically adjust the layout for LTR/RTL.