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ś