Stele - Home

Components
HomeWhat is stele?InstallationThe ProblemsThe PartsHow it worksTranslating React components.Translating strings outside of react components.Limitations in JSXHow Stele Works
How To
LicenseTechdebt

Stele

What is stele?

Stele is a suite of tools for building international applications in modern single page apps. The name comes from the Rosetta Stone, which was a Stele, meaning a giant stone (or wooden) monument, often with some sort of decree. Really, it was the coolest name we could think of that was not taken on NPM.

Installation

Currently our only dependency to use Stele is babel. If you are not currently using babel for your build, you can get started here

First install Stele:

$ npm i -D @patreon/stele

Then add it as a plugin to your babel config:

const { plugin } = require('@patreon/stele')
module.exports = {
presets: ['@babel/preset-react'],
plugins: [[plugin, options]],
}

The Problems

The ecosystem in javascript for internationalization is quite daunting to first look into. At Patreon we had a few main goals for starting our internationalization journey.

  1. We have thousands of untranslated strings.
  2. We have dozens of developers working at the same time.
  3. We have tens of thousands of strings and do not want to bloat our application by sending down strings we do not need.

Our goal is not to create a new standard to replace all standards in the JS ecosystem. We wanted to solve the problems we were facing at Patreon and share our solution. If you are facing a different set of problems this may not be the right solution for you or your product.

What we needed in an internationalizable library

  1. Transitioning strings should be as easy as possible.
  2. Writing translatable strings should be as easy as possible.
  3. Use the power of our compiler to alleviate run time checks of strings.
  4. Easily enforce conversion of our site

The Parts

  1. Babel plugin for compiling individual languages
  2. Webpack plugin for extracting a JSON file (coming soon!)
  3. An abstraction of ICU strings to aid in writing more complex international strings
    1. Documentation on those abstractions is available here

How it works

This library is heavily inspired by elm-i18n and i18n-webpack-plugin. This library co-evolved with js-lingui with similar ideas. When babel starts, so does Stele:

  1. Extracts that default language string to a JSON store
  2. Appends a [defaultLanguage]-[defaultLocale].json to your webpack build (coming soon!)
  3. Looks up strings from [currentLanguage]-[currentLocale].json
    1. Replace strings directly for namespaced functions
    2. Replace strings through a reverse compiler for React components
  4. Finally emit the new javascript file.

Translating React components.

Most of your strings will likely exist through React components. Stele makes transitioning these components easy.

Given:

<Text>
{props.food} is just a {props.kind} calzone
</Text>

Let's say that you have a Text component for rendering out user facing copy on your website. We need a way to tell the compiler that this text should be extracted to a JSON file and not just normal layout text. The way we will do this in react is through a prop on your component, let's call it intl. In order to get the above string translated in Stele the migration is simple:

<Text intl>
{props.food} is just a {props.kind} calzone
</Text>

This produces the JSON file:

{
"{food} is just a {kind} calzone": "{food} is just a {kind} calzone"
}

More complex and nested examples

Let's say you have a string that has some italic text in it

<Text>
Tom considers himself a <Text italic>foodie</Text>
</Text>

We want this entire string to be translatable, rather than translating "Tom considers himself a" and "foodie" stele instead extracts this as a single string once it is marked for translation:

<Text intl>Tom considers himself a <Text italic>foodie</Text></Text>
// creates json:
{"Tom considers himself a <1>foodie</1>": "Tom considers himself a <1>foodie</1>"}

Stele keeps an internal order for HTML tags when compiling back to the language it cares about. This way in case the order changes, stele can put the right JSX in the correct spots.

Built in components

Stele tries to support all ICU formats like select, number, date and plural. Using these in JSX is done through React components maintained by stele.

<Text>
Wait, now we're on an island? With
{ props.kidCount === 1 ? 'a kid' : 'kids') }
</Text>

Pluralizing this string in Stele takes a little bit more work than our previous examples. The big problem here is that while english only has 2 ways to pluralize something (One or Other), many languages have multiple. To handle this we simply need to use the plural component that Stele provides:

<Text intl intl-description="Phrase with plural check on main page">
Wait, now we're on an island? With
<Plural value={kidCount} one="a kid" other="# kids" />?
</Text>

This produces the ICU message we want to send our translators:

Wait, now we're on an island. With {kidCount, plural, one {a kid} other {# kids}}?

The translators will send back a string in a new language that might have different kinds of plurals, for instance in russian the string sent back might look like:

Подождите, теперь мы на острове. {kidCount, plural, {
=0 {Без детей}
one {C одним ребенком}
few {C # детьми}
many {C # детьми}
}}

When stele is compiling the russian version of the site, we compile that in to JSX for you that looks like:

<Text>
Подождите, теперь мы на острове?
<Plural
value={kidCount}
zero="Без детей"
ones="C одним ребенком"
few="C # детьми"
many="C # детьми"
/>?
</Text>

Translating strings outside of react components.

Let's say you have an input on your page with a placeholder:

<label>What if I get drunk and I talk about Darfur too much?</label>
<input placeholder="Have a practice date" />

Internationalizing the label is easy, however internationalizing the placeholder is just as easy:

<label>
<Text intl>What if I get drunk and I talk about Darfur too much?</Text>
</label>
<input placeholder={__('Have a practice date')} />

Dynamic strings

Sometimes you might need to include variables in your strings.

const whichWife = `No, my other ex-wife Tammy - Tammy ${whichTammy}.`

Internationalizing this follows standard ICU string rules:

const whichWife = __('No, my other ex-wife Tammy - Tammy {whichTammy}', {
whichTammy: whichTammy,
})

These strings are extracted directly with no manipulations, unlike JSX strings. We call this function the dunder function as a portmanteau of "double" and "underscore".

Limitations in JSX

While this appears to be a silver bullet, there are few common paradigms in JSX that will not work in Stele. Namely that no run time specific code can be put in our Text as children and also that strings are not computed at compile time, like they might in more idiomatic React code.

The rule

Strings in JSX must be wholly comprised of Stele components or string literals. Outside of JSX the entire string you want to display to a user must be a string literal.

An example of how to refactor an existing Text component

Let's say we had a string that computes a different string based on a variable:

<Text intl>But babe, {isBenWyatt ? 'calzone' : 'pizza'}</Text>

would only produce the message:

"But babe, "

because the above code relies on runtime behavior to create a string. All strings in Stele must be complete sentences, without logic.

Instead you could encompass both strings in their entirety inside of the ternary:

{
isBenWyatt ? (
<Text intl>But babe, calzone</Text>
) : (
<Text intl>But babe, pizza</Text>
)
}

or, if you'd like, you can even use the dunder function:

<Text>{isBenWyatt ? __('But babe, calzone') : __('But babe, pizza')}</Text>

Which is the valid run time version of the previous string.

© Patreon