Skip to content

React Tutorial

Adrian Gaudebert edited this page Jul 5, 2019 · 4 revisions

Translate your React application with Fluent

A short introduction to Fluent

Fluent is a localization technology, aimed at becoming a standard, used at Mozilla to replace older, less powerful localization tools. It is made for localizing software, and has various implementations, including one for React.

This tutorial is going to walk you through setting up and using Fluent to translate your React-based web application.

When dealing with localization, there are lots of decisions that need to be made:

  • What language will I show the user?
  • How will I get the translated content?
  • Where should I put the translation files?

Fluent developers decided that they would not make these decisions for you. Thus getting started with Fluent is a tad difficult, as it requires that you set those things up. The good news is, there are tools to help you make that easier.

Installation

Let's get started and install the tools we're going to need for this tutorial:

npm install --save fluent fluent-langneg fluent-react intl-pluralrules
# OR
yarn add fluent fluent-langneg fluent-react intl-pluralrules

Here's what each one is for:

  • fluent is the core Fluent JavaScript library, that will parse your language files and format translations.
  • fluent-langneg is a tool to choose which language to show a user, based on their browser's configuration and a list of supported locales.
  • intl-pluralrules contains polyfills for the Plural Rules Internationalization API that shipped in recent versions of our favorites browsers.
  • And finaly fluent-react provides React bindings for all things Fluent. It is responsible for replacing your application's content with localized strings.

I'm going to work from the bottom-up for this tutorial. First we'll mark a string as localizable, and then we're going to work upwards until we get to a point where the string is shown in the language your user favors.

Localizing Content

The very first step is to mark a piece of content as localizable. We're going to use fluent-react here, as it exposes a simple-to-use Localized component that does just that:

import React from 'react';
import { Localized } from 'fluent-react';

export default class HelloWorld extends React.Component {
    render() {
        return <Localized id="hello-world">
            <p>Hello, World!</p>
        </Localized>;
    }
}

There are two important bits here. The first one is the content of the Localized component. Notice that there's markup in there? That is fine: Localized will see that and only replace the actual text content of its child. This will lead to pretty awesome use cases that I'll get to later in this tutorial.

The second thing to note, and the most important one, is the localization id — or l10n-id in short — here defined using the id attribute of Localized. This is how your Fluent message is going to be referred to in the localization files, what localizers will see when translating, and what tools will use to keep context around your content (like history). You are free to use any value you want for these, but we at Mozilla have agreed on a social contract between developers and localizers. More on that later.

Creating a Context

Now that we have some localizable content, we need to wrap it in a localization context. That context will inject translations, which will be used to generate the translated content.

import React from 'react';
import { LocalizationProvider } from 'fluent-react';

export default class App extends React.Component {
    render() {
        const bundles = [];
        return <LocalizationProvider bundles={ bundles }>
            <HelloWorld />
        </LocalizationProvider>;
    }
}

The LocalizationProvider component takes an ordered list of bundles (a bundle is, essentially, a list of translations for a given locale) and makes it available to all Localized components in its tree. This code is simple but it does nothing, because we don't pass any bundles.

Providing Translations

We now need to provide bundles to our application. A bundle (an instance of FluentBundle) is a collection of messages for a locale, each message being a localized content in that locale. Let's start by building a naive list of bundles:

import { FluentBundle } from 'fluent';

const MESSAGES_ALL = {
    'en-US': 'hello-world = Hello, World!',
    'fr': 'hello-world = Salut le monde !',
};
const LOCALES_ALL = ['fr', 'en-US'];

function generateBundles() {
    return LOCALES_ALL.map(locale => {
        const bundle = new FluentBundle(locale);
        bundle.addMessages(MESSAGES_ALL[locale]);
        return bundle;
    });
}

With this, we can now pass our localized content to our application. The order of locales in LOCALE_ALL determines which locale will be shown first, and what is the order of fallbacks in case a particular message is missing in a locale. Let's use that in our app to get a working example:

import React from 'react';
import { FluentBundle } from 'fluent';
import { LocalizationProvider, Localized } from 'fluent-react';


const MESSAGES_ALL = {
    'en-US': 'hello-world = Hello, World!',
    'fr': 'hello-world = Salut le monde !',
};
const LOCALES_ALL = ['fr', 'en-US'];

function generateBundles() {
    return LOCALES_ALL.map(locale => {
        const bundle = new FluentBundle(locale);
        bundle.addMessages(MESSAGES_ALL[locale]);
        return bundle;
    });
}


function HelloWorld() {
    return <Localized id="hello-world">
        <p>Hello, World!</p>
    </Localized>;
}


function App() {
    return <LocalizationProvider bundles={ generateBundles() }>
        <HelloWorld />
    </LocalizationProvider>;
}

This is the minimum amount of code you need to have some content translated. In this case, it will show the French translation: "Salut le monde !" But this is very simple, and in the real world, we'll need to do more than just that.

There are three things you are likely to want:

  1. Loading translations from files.
  2. Having fallbacks in case of not fully supported locales.
  3. Figuring out what locales to serve to the user.

Storing Translations

The convention for storing Fluent translations is to put them in .ftl files. You should have a reference locale (for example, en-US) which you will have to keep in sync with your code.

If your application is small, you can then create a file per locale you want to support:

locale/
    ar.ftl
    en-US.ftl
    fr.ftl

However, if your app is big or is bound to grow bigger, I'd recommend you create a folder per locale. That way you can easily split your translations into multiple files, for example one file per page, or even one per module if that makes sense.

locale/
    ar/
        content.ftl
    en-US/
        content.ftl
    fr/
        content.ftl

Loading translations from files is a simple matter of using fetch or whichever network tool you like. .ftl files are simple text files, and the Fluent parser will take care of understanding these for you. Here's an example of a function that fetches a locale file:

async function getMessages(locale) {
    const url = `/static/locale/${locale}/content.ftl`;
    const response = await fetch(url);
    return await response.text();
}

Let's now use that knowledge to update the previous code example:

const LOCALES_ALL = ['fr', 'en-US'];

async function generateBundles() {
    return LOCALES_ALL.map(locale => {
        const translations = await getMessages(locale);
        const bundle = new FluentBundle(locale);
        bundle.addMessages(translations);
        return bundle;
    });
}

Aside on Automatic String Extraction

If you're like me, you would be thinking: there has got to be an automated system out there to turn my <Localized> elements into some .ftl files. Well, it turns out that the Fluent ecosystem is very young, and there is only just one such system at the moment. Udacity has created fluent-react-utils, which has a string extraction feature. Be wary though that this is an extremely difficult thing to implement, and I can't guarantee that it will work in all cases. At Mozilla we chose not to use such a tool, and maintain our reference files manually.

Falling Back

You might have noticed that we pass a list of bundles to the LocalizationProvider component. Why not just pass the one locale the user cares about? Because some of your locales might be 100% translated. When that happens, you will likely want to show the user a good fallback.

For example, say your app is translated into several variants of Spanish: es-ES (Spain) and es-AR (Argentina). When a user from Argentina uses your application, you'll want to show them strings in es-AR. But if a string is not translated, instead of showing the English string, it is probably better to check if that string exists in es-ES and show that instead. Chances are it will be a much better experience for your user.

Here's an example to show you how the fallback system works with fluent-react. First the updated component, with new strings:

function HelloWorld() {
    return <div>
        <Localized id="hello-world">
            <p>Hello, World!</p>
        </Localized>
        <Localized id="just-met-you">
            <p>Hey, I just met you!</p>
        </Localized>
        <Localized id="it-s-crazy">
            <p>And this is crazy…</p>
        </Localized>
    </div>;
}

Then our .ftl files, containing translations:

en-US.ftl:

hello-world = Hello, World!
just-met-you = Hey, I just met you!
it-s-crazy = And this is crazy…

fr.ftl:

hello-world = Salut le monde !
just-met-you = Hé, on vient de se rencontrer !

it.ftl:

hello-world = Buongiorno Mondo!

Now let's change our locales list to this:

const LOCALES_ALL = ['it', 'fr', 'en-US'];

If we were to run our code with these changes, it would show this result:

Buongiorno Mondo!
Hé, on vient de se rencontrer !
And this is crazy…

Italian only has the first translated string, that one is displayed as expected. For the second one, it will fallback to the second locale, French, and find a result. For the third one, Italian doesn't have it, neither does French, it will thus fall back to the English.

Note: A rule of thumb is to always have your reference locale as the last element of the locales list, because that one should always have all strings. When a string is missing entirely, fluent-react will show the content of your Localized component, but without doing anything on it. So if your string requires some computations, or has variables, they will be shown as-is to the user, which I assume you want to avoid.

Language Negotiation

It's great to have translated content, but it's useless if we can't determine which locale to serve the user. This is where language negotiation comes into play. There are many ways to do it though, so I'll present only the most common use case. It's simple: we use whatever the browser says are the user's favorite locales. That is easy to do with fluent-langneg:

import { negotiateLanguages } from 'fluent-langneg';

const languages = negotiateLanguages(
    navigator.languages,
    
    // This is your list of supported locales.
    AVAILABLE_LOCALES,
    
    // Setting defaultLocale to your reference locale means that it will
    // always be the last fallback locale, thus making sure the UI is
    // always working.
    { defaultLocale: 'en-US' },
);

The negotiateLanguages function will create an ordered list of locales based on the user's preferences, the list of available locales, and other options like the default locale in the example. If the user's preferences are fr-BE then fr-FR then fr, and the available languages are [ 'fr-BE', 'fr', 'en-US' ], the function will return [ 'fr-BE', 'fr', 'en-US' ].

Summary

With this, you should now understand the most important parts of localizing an application with Fluent and fluent-react. Create localizable content with Localized, assign an identifier to each piece of content, create a context that contains the translations for each supported locale, in the order you want them shown. That's it!

Let's take a look at a complete example using everything we've seen so far:

import React from 'react';
import { FluentBundle } from 'fluent';
import { negotiateLanguages } from 'fluent-langneg';
import { LocalizationProvider, Localized } from 'fluent-react';


// List all available locales.
const AVAILABLE_LOCALES = ['it', 'fr', 'en-US'];


// Negotiate user language.
const languages = negotiateLanguages(
    navigator.languages,
    AVAILABLE_LOCALES,
    { defaultLocale: 'en-US' },
);


// Load locales from files.
async function getMessages(locale) {
    const url = `/static/locale/${locale}/content.ftl`;
    const response = await fetch(url);
    return await response.text();
}


// Generate bundles for each locale.
async function generateBundles() {
    return languages.map(locale => {
        const translations = await getMessages(locale);
        const bundle = new FluentBundle(locale);
        bundle.addMessages(translations);
        return bundle;
    });
}


// Show localized content.
function HelloWorld() {
    return <div>
        <Localized id="hello-world">
            <p>Hello, World!</p>
        </Localized>
        <Localized id="just-met-you">
            <p>Hey, I just met you!</p>
        </Localized>
        <Localized id="it-s-crazy">
            <p>And this is crazy…</p>
        </Localized>
    </div>;
}


// Create localization context and render content.
function App() {
    return <LocalizationProvider bundles={ generateBundles() }>
        <HelloWorld />
    </LocalizationProvider>;
}

Going Further

Localizing Attributes

Localizing attributes of any React component is fairly easy with fluent-react:

<Localized id='my-message' attrs={ { title: true } }>
    <a href='link' title='Link to something'>Something</a>
</Localized>

Simply pass an attrs prop containing an object with, for each attribute you want to localize, the name of that attribute as key and true as value. Then, in your .ftl file, here's what your message will look like:

my-message = Something
    .title = Link to something

See the official documentation if you want to know more about Fluent's Syntax. Hint: it's great and full of very useful features!

Use Semantic Identifiers

I mentioned earlier that, at Mozilla, we had defined a "social contract" between developers and localizers. This social contract is established by the selection of a unique identifier, called l10n-id (for localization identifier), which carries a promise of being used in a particular place to carry a particular meaning.

You should consider the l10n-id as a variable name. If the meaning of the content changes, then you should also change the ID. This will notify localizers that the content is different from before and that a new translation is needed. However, if you make minor changes (fix a typo, make a change that keeps the same meaning) you should instead keep the same ID.

An important part of the contract is that the developer commits to treat the localization output as opaque. That means that no concatenations, replacements or splitting should happen after the translation is completed to generate the desired output.

In return, localizers enter the social contract by promising to provide an accurate and clean translation of the messages that match the request.

In Fluent, the developer is not to be bothered with inner logic and complexity that the localization will use to construct the response. Whether declensions or other variant selection techniques are used is up to a localizer and their particular translation. From the developer perspective, Fluent returns a final string to be presented to the user, with no l10n logic required in the running code.