Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

fluent-react: React bindings for Fluent

129 views
Skip to first unread message

Staś Małolepszy

unread,
Feb 23, 2017, 4:19:00 PM2/23/17
to tools...@lists.mozilla.org
Hi all --

Fluent is the new name for the low-level API powering L20n. Fluent is
small and very unopinionated. It makes it easy to integrate it with
other stacks.

I wrote a prototype of React bindings for Fluent:

https://github.com/projectfluent/fluent.js/tree/master/fluent-react

It takes advantage of the composition pattern to wrap elements and
components and provide translations for them:

<LocalizedElement id="welcome" $username={name}>
<p>{'Welcome, {$username}'}</p>
</LocalizedElement>

It's an early version and the API will likely change. I would love to
get feedback from people familiar with React. Bonus points for working
on an app which ships in multiple languages :)

The README in the repo explains the basics. I wrote a number of small
examples to demonstrate what's currently implemented:

https://github.com/projectfluent/fluent.js/tree/master/fluent-react/examples

Lastly, there's a rather lengthy design / braindump doc on the wiki:

https://github.com/projectfluent/fluent.js/wiki/React-bindings-for-Fluent

Hope you like it!
Staś


* * *

Context & History:

Nearly a year ago we had an in-depth discussion about how we might
integrate L20n into React:

https://groups.google.com/forum/#!topic/mozilla.tools.l10n/XtxHgBEokCA

One take-away from that thread was that perhaps we could just let L20n
and React live alongside each other. After all, L20n uses its own
MutationObserver to detect changes to the native DOM and could
localize DOM elements after React has rendered them.

Today we can say that it wasn't the best idea: React and L20n can race
against each other or cause the content to be rendered twice. L20n
also removes <!-- react-text --> comments from the DOM which may cause
trouble with React's reconciliation algorithm.

In January I wrote a post in this newsgroup introducing Project Fluent

https://groups.google.com/forum/#!topic/mozilla.tools.l10n/NPmsJD4IGjQ

Fluent takes the low-level API from L20n and packages it separately so
that it's easy to work with and integrate in larger codebases. You
can read more about Fluent and L20n on the wiki:

https://github.com/projectfluent/fluent/wiki/Fluent-and-L20n

To learn more about Fluent, see http://projectfluent.io/.

Staś Małolepszy

unread,
Feb 26, 2017, 12:04:34 PM2/26/17
to tools...@lists.mozilla.org
As fluent-react is an early prototype, I still have many open
questions about its design. I'll summarize them below in hope that
the React community can help me find the best path forward.


1. Use-cases

I wrote fluent-react with the assumption that everything in the UI is
expressed as a React element, be it DOM or Component. In what cases
does this assumption break? For instance, do you keep default
placeholders in your app's or component's state?

What we learned during the Firefox OS days is that almost every time
you're tempted to store a translation value somewhere, you may just as
well store the identifier of the translation and let the localization
library format the translation later.

Does this hold for React as well? One of the examples I wrote
demonstrate the idea behind this:

https://github.com/projectfluent/fluent.js/blob/236c270971ab2ee88b7d3357860d065ec068a985/fluent-react/examples/text-input/src/App.js#L25-L33

Instead of storing unlocalizable "stranger" in the state, there are
two LocalizedElements, one for when the name is known and one for when
the name is unknown. This has two benefits: the localizer can
customize the unknown-name case to make it sound more natural, and
both options exist in the source code which allows for automatic
extraction of localizable copy.

I'd like to know if this would work in large React apps.


2. Pluggable components

I don't know what to call them, but what I mean are small components
that can be installed via npm and used in your app. Is it safe to
assume that most of them can be localized by passing props? An
example of this approach is React Toolbox's Snackbar:

http://react-toolbox.com/#/components/snackbar

<Snackbar
action='Nice'
label='A new developer started using React Toolbox'

/>

With fluent-react, I'd localize it via:

<LocalizedElement id="snackbar-nice">
<Snackbar
action='Nice'
label='A new developer started using React Toolbox'

/>
</LocalizedElement>

And the Fluent translation would look like this:

snackbar-nice
.action = Nice
.label = A new developer started using React Toolbox

One counter-example which I was able to find also comes from React
Toolbox: it's the Dialog component.

http://react-toolbox.com/#/components/dialog

Its `actions` prop is an array of objects with localizable properties:

actions = [
{ label: "Cancel", onClick: … },
{ label: "Save", onClick: … }
];

<Dialog
actions={this.actions}
title='My awesome dialog'

>

</Dialog>

My recommendation would be to change the API of Dialog and make
`actions` accept React elements, which could then be wrapped in
LocalizedElement themselves. Something like:

actions = [
<LocalizedElement id="dialog-action-cancel">
<DialogAction onClick={…}>Cancel</DialogAction>
</LocalizedElement>
<LocalizedElement>
<DialogAction onClick={…}>Save</DialogAction>
</LocalizedElement>
];

<Dialog
actions={this.actions}
title='My awesome dialog'

>

</Dialog>

An alternative solution would be to use a custom Dialog component in
which the number of actions is predefined and localize the actions via
props.

Can you think of other components that wouldn't work well with the
localization-via-props approach?


3. Fetching translations

There are many different approaches to loading the right translations
in the app. I ran a quick Twitter poll yesterday and from the ~100
responses I got so far, it looks like the most common approach is to
do an async fetch on the clientside once the preferred language of the
user is known.

https://twitter.com/stas/status/835483537105170432

Right now, fluent-react's LocalizationProvider takes a
`requestMessages` prop which is a function returning a promise. I'd
like to simplify this and use a pattern that I think is common in
React: use wrapping and lifecycle methods to fetch the translations
any way you want.

import { LocalizationProvider } from 'fluent-react';
import negotiateLanguages from 'fluent-langneg';

// Provide your own implementation.
import fetchMessages from './lib/l10n';

class MyLocalizationProvider extends Component {
constructor(props) {
super(props);
this.state = {
locales: negotiateLanguages(props.requested),
messages: ''
};
}

componentWillMount() {
fetchMessages(this.state.locales).then(
messages => this.setState({ messages })
);
}

render() {
const { children } = this.props;
const { locales, messages } = this.state;

if (!messages) {
// Show a loader?
return null;
}

return (
<LocalizationProvider locales={locales} messages={messages}>
{children}
</LocalizationProvider>
);
}
}

You'd use like this:

ReactDOM.render(
<MyLocalizationProvider requested={navigator.languages}>
<App />
</MyLocalizationProvider>,
document.getElementById('root')
);

Is this too much boilerplate to ask the developers to write? I'd like
to allow maximum flexibility so that a large variety of use-cases and
setups can be supported. For instance, it would be easy to have a
small sync LocalizationProvider for the word "Loading" in all
supported languages, and a bigger async one for other messages.

There's also a question of runtime fallback. What should happen when
a translation is missing in the current language? We could make
LocalizedElement request the translation asynchronously from
LocalizationProvider, although that might lead to having a lot of
Promises in the running code. I'd need to measure the performance
impact of such approach. It could be introduced later, too, available
as a flag on LocalizationProvider (e.g. runtimeFallback={true}).

Another alternative is to make this a responsibility of the build
setup: before publishing, any translations could be merged with the
default language to fill in the gaps. For even better support of
non-standard fallback orders (i.e. not falling back directly to
English), ServiceWorkers could be used to perform the merging
"build-step" for translations.


4. Naming

Lastly, is LocalizedElement a good name? Throught this name I wanted
to emphasize that it takes a single React element as a child. It's a
bit long, especially if it's going to be used a lot. Perhaps
something simple like Localized or Localizable would be enough?


Thanks for reading all of this and for trying fluent-react out!
Staś

Staś Małolepszy

unread,
Mar 6, 2017, 4:26:15 PM3/6/17
to tools...@lists.mozilla.org
On Sun, Feb 26, 2017 at 6:04 PM, Staś Małolepszy <st...@mozilla.com> wrote:

> 3. Fetching translations
>
> Right now, fluent-react's LocalizationProvider takes a
> `requestMessages` prop which is a function returning a promise. I'd
> like to simplify this and use a pattern that I think is common in
> React: use wrapping and lifecycle methods to fetch the translations
> any way you want.

I went ahead and removed `requestMessages` in favor of a simpler API:
LocalizationProvider now takes messages as a prop. The task of
fetching translations asynchronously is now in developers' hands which
allows much more flexibility. See
https://github.com/projectfluent/fluent.js/blob/master/fluent-react/examples/async-messages/src/l10n.js#L25-L55
for an example code.

If you npm install fluent-react, version 0.1.0 has this new API:

https://github.com/projectfluent/fluent.js/blob/master/fluent-react/CHANGELOG.md#fluent-react-010

> There's also a question of runtime fallback. What should happen when
> a translation is missing in the current language? We could make
> LocalizedElement request the translation asynchronously from
> LocalizationProvider, although that might lead to having a lot of
> Promises in the running code. I'd need to measure the performance
> impact of such approach. It could be introduced later, too, available
> as a flag on LocalizationProvider (e.g. runtimeFallback={true}).

I'm planning to write an experimental AsyncLocalizationProvider which
uses a Promise per LocalizedElement. I'd like to test the performance
cost of such an approach. In L20n.js we introduced an optimization:
we'd collect identifiers first and then use a single Promise to format
all of them. I'm not sure how to do this right in React right now.

> Another alternative is to make this a responsibility of the build
> setup: before publishing, any translations could be merged with the
> default language to fill in the gaps. For even better support of
> non-standard fallback orders (i.e. not falling back directly to
> English), ServiceWorkers could be used to perform the merging
> "build-step" for translations.

Once I've written the FTL serializer in JavaScript, I'll publish a
tool to merge incomplete localization with more complete ones. Expect
more news by the end of this week.

Staś Małolepszy

unread,
Apr 19, 2017, 10:49:53 AM4/19/17
to tools...@lists.mozilla.org
I've published fluent-react 0.3.0 today. This version is not
compatible with the previous ones.

The messages prop passed to LocalizationProvider should now be an
iterable of MessageContext instances in order of the user's preferred
languages. The MessageContext instances will be used by Localization
to format translations. If a translation is missing in one instance,
Localization will fall back to the next one.

In its simplest form, the messages prop can be an array:

function generateMessages(currentLocales) {
return currentLocales.map(locale => {
const cx = new MessageContext(locale);
cx.addMessages(MESSAGES_ALL[locale]);
return cx;
});
}

<LocalizationProvider messages={generateMessages(['en-US'])}>

</LocalizationProvider>

In order to avoid having to create all MessageContext instances for
all locales up front, it's recommended to make the messages prop an
iterator. The Localization class will iterate over it (memoizing the
items it yields) in case fallback is required.

function* generateMessages(currentLocales) {
for (const locale of currentLocales) {
const cx = new MessageContext(locale);
cx.addMessages(MESSAGES_ALL[locale]);
yield cx;
}
}

<LocalizationProvider messages={generateMessages(['en-US'])}>

</LocalizationProvider>


This new design of the LocalizationProvider requires a little bit of
work from the developer. The messages iterable needs to be created
manually. This is intentional: it gives the most control to the
developer with regards to the following three areas:

- translations - the developer decides where translations are stored
and how they're fetched,

- language negotiation - the developer decides which factors are
taken into account for the purpose of the language negotiation, as
well as how exactly it's being done. This allows to store the user's
preference in cookies, localStorage or a server session. I recommend
fluent-langneg for the actual negotiation.

- custom extensions - the developer can pass options to the
MessageContext constructor to configure its behavior or to define
functions available to translations.

In the future we might end up providing ready-made generators of the
messages iterable for the most common scenarios.

One limitation of the current design is that in asynchronous
scenarios, all translations (including any fallback) must be fetched
at once before <LocalizationProvider> is rendered. In the future we
might be able to allow async fetching of fallback locales (possibly
using async generators).

See the README for more information and examples of use:

https://github.com/projectfluent/fluent.js/tree/bd10187207d16a9c66e1acc10230e083fcab1dca/fluent-react

All examples have been updated to use the new API:

https://github.com/projectfluent/fluent.js/tree/bd10187207d16a9c66e1acc10230e083fcab1dca/fluent-react/examples
0 new messages