Skip to content

Integrating Fluent — Overview

Stanisław Małolepszy edited this page Jun 9, 2020 · 8 revisions

This article is meant for developers looking into integrating Fluent into their codebases and workflows. For an introduction to our syntax, go to the Fluent Syntax Guide.

Fluent is a platform-agnostic localization system. Our initial target platform selection is a good starting point for both, understanding how Fluent integrates into codebases and workflows, and how it should be ported to other platforms.

In the tutorial below, we focus on three targets - a Web application, a React application and a JS application. For other platforms and use cases, consult Integrating Fluent — Use Cases.

Declarative vs. Imperative

All user interface models can be divided into declarative and imperative. Declarative markup describes the user interface with a dataset, while imperative introduces it via running code.

An example of a declarative UI is HTML:

<header>
  <h1>Hello World</h1>
  <button title="Click to open menu" accesskey="C">Click me</button>
</header>

An example of an imperative UI is a JavaScript code:

console.log("Hello World");
console.log("You have 5 unread messages.");

Fluent aims to support both models, but strongly recommends describing user interfaces using the declarative approach.

JavaScript

In cases where the UI is built imperatively via running code, Fluent provides a low and high level translation APIs.

Low-Level API

The low level API is meant to be simple, yet provide all features of Fluent, while the high level adds more sophisticated features such as asynchronous language fallbacking.

Example of the Fluent API:

let resource = new FluentResource(`
hello-world = Hello World
unread-messages = You have { $unreadCount ->
    [one] unread message
   *[other] unread messages
}
`);

let bundle = new FluentBundle(["en"]);
bundle.addResource(resource);

let helloWorld = bundle.getMessage("hello-world");
console.log(bundle.formatPattern(helloWorld.value));

let unreadMessages = bundle.getMessage("unread-messages");
console.log(bundle.formatPattern(unreadMessages.value, {unreadCount: 5}));

The core API is intentionally not opinionated and does not handle aspects such as loading .ftl files in order to keep it open to fit into any software model and allow users to select how and when they want to generate and store FluentBundle objects.

For an example of the low level API see the JS implementation: @fluent/bundle.

High-Level API

Higher level API, an example of which is a Localization class in the @fluent/dom package is wrapping the low level API offering hooks use for resource loading, language negotiation and lazy language fallbacking.

An example of such API use:

const AVAILABLE_LANGS = ["de", "en", "fr"];

async function * generateBundles(resourceIds) {
  let languages = negotiateLanguages(navigator.languages, AVAILABLE_LANGS);
  for (const lang of languages) {
    let bundle = new FluentBundle(lang);
    for (let resourceId of resourceIds) {
      let source = await loadFile(resourceId);
      let resource = new FluentResource(source);
      bundle.addResource(resource);
    }
    yield bundle;
  }
}

let l10n = new Localization([
  '/browser/main.ftl',
], generateBundles);

let msg = await l10n.formatValue('hello-world');
console.log(msg);

The generateBundles generator function allows for maximum flexibility when integrating the system into your workflow, but will probably be written once per application, and only the last 10 lines of the example represent what the developer will work with.

The example above is both lazy and asynchronous, but depending on your project requirements and environment you may alter it to your needs.

For more information about Localization and DOMLocalization, see @fluent/dom.

HTML

In the declarative approach, Fluent introduces a binding between a UI widget and its localizations in form of a unique identifier. In HTML we chose data-l10n-id (although using namespaces has been considered), but each environment may chose their own binding scheme.

An example of such binding is:

hello-world = Hello World
click-button = Click me
    .title = Click to open a menu
    .accesskey = C
<header>
  <h1 data-l10n-id="hello-world"></h1>
  <button data-l10n-id="click-button"></button>
</header>

All other declarative markup languages can use analogous binding schemes to describe a relation between a UI widget and its translations. This model is very flexible and allows for very rich error recovery.

For more examples, see the experimental @fluent/web package.

React

@fluent/react is an example of how we envision Fluent fitting into an existing UI framework workflow, and you'll find it similar to the examples above with a focus on declarative UI approach.

In case of React, we use higher-order components to create bindings between the declarative UI and localizations. An example of such binding looks like this:

export function App() {
  return (
    <div>
      <Localized id="title">
        <h1>Hello, world!</h1>
      </Localized>
    </div>
  );
}

You may recognize the idea of the binding from the HTML example, but it's heavily adapted to the React workflow. Fluent can be fit into many different frameworks by using the low level API and building a higher level bindings on top of it.

For more information, refer to the @fluent/react package docs.