react-native-l20n

Mozilla's L20n localization framework for React Native

Usage no npm install needed!

<script type="module">
  import reactNativeL20n from 'https://cdn.skypack.dev/react-native-l20n';
</script>

README

react-native-l20n

Experimental adaptation of Mozilla's L20n localization framework for use within React Native.

npm install --save react-native-l20n
import React, {Component} from 'react';
import {View, Text} from 'react-native';
import L20n, {ftl} from 'react-native-l20n';

const l20n = L20n.create({
  en: ftl`
    product = react-native-l20n
    welcome = Welcome, {$name}, to {product}!
    description =
      | {product} makes it possible to harness the forward-thinking
      | design of Mozilla's L20n in an idiomatic fashion.
    stars = This repository has {$count ->
      [0] no stars
      [1] one star
     *[other] {$count} stars
    } on GitHub.
  `,

  es: ftl`
    welcome = Bienvenidos, {$name}, a {product}!
  `,
});

class Example extends Component {
  render() {
    return (
      <View>
        <Text style={{fontWeight: 'bold'}}>
          {l20n.welcome({name: 'James'})}
        </Text>
        <Text>
          {l20n.description()}
        </Text>
        <Text>
          {l20n.stars({count: 1000})}
        </Text>
      </View>
    );
  }
}

Why L20n?

Mozilla has decades of experience shipping localized products. The design of L20n reflects this accumulation of experience, and manages to deliver a format as powerful as ICU MessageFormat, but as simple as gettext.

If these comparisons mean nothing to you, perhaps it will suffice to say that L20n makes it easy to isolate the strings in your application, perform basic variable substitutions, and handle language nuances like pluralization, gender, and declension.

You don't take my word for it, though. Here are three excellent resources for getting started with L20n:

  • Learn the syntax with this quick step-by-step guide.

  • Tinker with framework with this browser-based IDE.

  • Read about the decisions that underpin the powerful, asymmetric design of the framework in this blog post.

What's different for React Native?

The main drawback of L20n, from my perspective, is that it takes a heavy dependency upon the DOM as its formal interface. Just as StyleSheet brought the best of CSS for use in React Native, this module decouples L20n from the DOM and makes it available to your React Native app through a familiar, idiomatic interface.

The first similarity to StyleSheet is that L20n translations are meant to be declared within the component they're used, alongside styles. For example:

const styles = StyleSheet.create({...});
const l20n = L20n.create({...});

As a consequence, nothing in the React Native implementation of L20n is asynchronous, which means that the interface for accessing translations is a simple, synchronous function that returns a string, like such:

render() {
  return (
    <Text style={styles.text}>
      {l20n.helloWorld()}
    </Text>
  );
}

As seen in this example, the React Native implementation of L20n does not utilize data attributes (or any annotations in the virtual DOM or JSX) to look up translations; it's just simple function calls, which means it can be used with any component, builtin or third party.

My advice is to generally ignore the API documentation on Mozilla's L20n website with the exception of their guide to FTL, the L20n translation format.

Finally, it's worth noting that L20n depends upon the ECMAScript Internationalization API (found in browsers under window.Intl), which is provided via polyfill. This module also removes bidirectional isolation characters which are inserted by L20n, but not supported by either React Native platform.

API

L20n.create(translations)

translations is an object that maps locales to translations.
Locales are specified as two-letter ISO 639-1 codes.
Translations are specified in L20n's FTL format.

L20n.create() returns an object that maps each translation key to a function. The function can be invoked with a single object argument to provide variables for substitution into the translated string.

When the function is invoked, a translation for the current locale is used; if none is available, the default locales are attempted in order. Failing that, the translation key is returned.

Example:

import L20n from 'react-native-l20n';

const l20n = L20n.create({
  en: `key = The value is: {$variable}`
  es: `key = El valor es: {$variable}`
});

console.log(l20n.key({variable: 'foo'));
// => "The value is: foo" if device is in English
// => "El valor es: foo" if device is in Spanish

L20.currentLocale

Get or set the current locale.
The locale is specified as a two-letter ISO 639-1 code.

The value is initialized to the locale of the device. If you wish to programmatically change it, do so before rendering your first component.

L20.defaultLocales

Get or set the default locales.
Locales are specified as two-letter ISO 639-1 codes.
These locales are attempted when a translation isn't available for the current locale.

Defaults to ['en']. If you wish to programmatically change it, do so before rendering your first component.

ftl

ES6 templated string tag for FTL, the L20n translation format.

The ftl tag is not required, but enables you to indent your translations, which is not normally legal. It also removes newlines from piped, multi-line translations, which emulates the whitespace-collapsing nature of HTML.

Example:

import {ftl} from 'react-native-l20n';

const translations = {
  en: ftl`
    firstKey = First
    secondKey =
      | This string spans
      |                   multiple lines.
  `,
};

console.log(translations.en);
// (The output is legal FTL.) =>
// firstKey = First
// secondKey =
// | This string spans multiple lines.

Future work

One of the creators of L20n, @stasm, has been exploring proposals for deeper integration of L20n into browser-based React. A very thorough series of proposals is under discussion on this thread.

The approach of this module is to hew as closely as possible to plain-old portable JavaScript, with some conveniences added to conform with React Native idioms. Perhaps L20n will formalize an API that requires neither DOM access or Node.js builtin modules, which would eliminate the need to vendor a modified version of the L20n framework. (This would likely involve isolating the FTL parser and runtime from the rest of L20n.)

Beyond that, there are a number of enhancements that could be added to this module to mature it into a scalable localization solution:

  • Handle the TODOs listed at the top of the source.

  • Generalize this module for use in browser-based React or apart from any framework. This would essentially substitute for the L20n Node.js interface, which is a bit lacking.

  • Build tooling to collect strings from components, and support loading/bundling of translations into separate files, apart from the component definitions.

  • Build a runtime inspector to identify translation keys.

More than anything, I'd appreciate your feedback on what it will take for this module to become a production-grade solution for your project. Please open an issue to discuss.