Stele - How Stele Works

Components
HomeHow Stele WorksCreating an ICU message
How To
LicenseTechdebt

How Stele Works

Stele is a compile-time translation library that can also operate at runtime. Stele works by taking the code you write and transforming it in to an ICU message. Then Stele can take ICU messages and convert that back in to run time code. This, however, gives Stele some limitations. I feel that understanding how Stele works can make it easier to understand why you have to make some small tweaks to your code.

Why it has to be this way?

Real humans are the ones most likely to be doing our translations. They need to be able to see the entirety of a message to correctly translate it. ICU messages are just a fancy way of doing string interpolation that human translators understand. ICU messages give us an API to carefully construct accurate messages to send to translators.

Let's break this down by parts:

Creating an ICU message

Stele looks at your code in an abstract way. It does not know what your code does, or why. All Stele knows is what it can see when webpack or babel compiles your application. Stele does this by using the Abstract Syntax Tree that babel provides. The babel team has a great guide on ASTs here.

I am writing this guide without the requirement of knowing more about ASTs however. Also, here we are only going to cover message extraction, not replacement.

Steps

  1. Turn Code in to an AST
  2. Filter down to only translatable JSX calls.
  3. Go through children and turn them in to corresponding ICU string.
  4. Extract out context from JSX call

A simple starting point

<p intl={locale}>We are having a sale all day today!</p>
<p>We don't need to translate this</p>

Once you mark this JSXElement as something translatable Stele gets to work. The first thing it does is convert your code to babel's AST, which very abstractly looks something like this:

{
openingTag: JSXOpeningTag('p'),
props: ObjectExpression([ObjectMemberExpression(StringLiteral('intl'), Identifier('locale'))]),
children: [StringLiteral('We are having a sale all day today!')]
}, {
openingTag: JSXOpeningTag('p'),
props: ObjectExpression(),
children: [StringLiteral("We don't need to translate this")]
}

Now our abstract syntax tree gives us something we can easily iterate and look through. The first thing Stele does is try to find all the JSX tags in your file. Once it does that it filters them down to only those who have your .babelrc configuration for what is to be internationalized. Say that is all JSXElements with a prop called intl that reduces our list to:

{
openingTag: JSXOpeningTag('p'),
props: ObjectExpression([ObjectMemberExpression(StringLiteral('intl'), null)]),
children: [StringLiteral('We are having a sale all day today!')]
}

Steps (1) and (2) are now complete!

For Step (3) Stele basically just has a fancy logic statement that looks like this in pseudocode:

const icuMessage = jsxElement.children.reduce((message, child) => {
// if the child is a string, just add it to the message
if (isStringLiteral(child)) {
message = message + child.value
}
// other if statements
return message
}, '')

So in our simple case above we create the message We are having a sale all day today!.

Handling arguments

Let's say we augment our original statement a little bit to have a sale on something specific:

<p intl={locale}>We are having a sale all day today on {category}!</p>

Getting through step (2) above leaves us with something like this:

{
openingTag: JSXOpeningTag('p'),
props: ObjectExpression([ObjectMemberExpression(StringLiteral('intl'), Identifier('locale'))]),
children: [StringLiteral('We are having a sale all day today '), Identifier('category'), StringLiteral('!')]
}

Now our original reduce method needs to be able to handle Identifiers and not just StringLiterals.

const icuMessage = jsxElement.children.reduce((message, child) => {
// if the child is a string, just add it to the message
if (isStringLiteral(child)) {
message = message + child.value
}
if (isIdentifier(child)) {
message = message + `{${child.name}}`
}
// other if statements
return message
}, '')

Now what we do is for an argument we create {category} and append it to the string. So in the loop it looks like this:

  1. ''
  2. StringLiteral('We are having a sale all day today ') -> 'We are having a sale all day today '
  3. Identifier('category') -> 'We are having a sale all day today {category}'
  4. StringLiteral('!') -> 'We are having a sale all day today {category}!'

Can Stele infer the value of a variable?

Let's say I had a really long string and I wanted to substitute part of it out I could do something like this:

const duration = `We are having a sale all day today on ${category}!`
<p intl={locale}>{duration}</p>

In this case does Stele know the full string is We are having a sale all day today on {category}!?

The answer is no, stele would produce the message:

{duration}

To figure out why let's look at what Stele sees:

{
openingTag: JSXOpeningTag('p'),
props: ObjectExpression([ObjectMemberExpression(StringLiteral('intl'), Identifier('locale'))]),
children: [Identifier('duration')]
}

Because Stele first finds JSX tags, it does not get the entire scope of the application. Therefore, Stele has no idea where duration is defined or what possible values it can have. The only information Stele has is that there is an identifier named duration in it's children. Babel does not send along information about where that value is defined, or what value it could possibly have.

Stele Components

Let's say we wanted to add a plural to our message. Let's say we wanted to have a sale on multiple categories:

<p intl={locale}>
We are having a sale all day today on{' '}
<Plural value={numCategories} one="something special" other="# things" />!
</p>

This gives Stele the following:

const ast = {
openingTag: JSXOpeningTag('p'),
props: ObjectExpression([
ObjectMemberExpression(StringLiteral('intl'), Identifier('locale')),
]),
children: [
StringLiteral('We are having a sale all day today '),
JSXElement(
Identifier('Plural'),
ObjectExpression(
MemberExpression(StringLiteral('one'), StringLiteral('something special'))
MemberExpression(StringLiteral('other'), StringLiteral('# things'))
)
)
StringLiteral('!'),
],
}

Now we need to do a little updating of the reducer function

const icuMessage = jsxElement.children.reduce((message, child) => {
/* stuff from above... */
if (isJSXElement(child)) {
// look inside child.openingTag if that matches a stele element continue
if (isSteleElement(child)) {
if (child.openingTag.name === 'Plural') {
return handlePlural(child)
}
}
}
// other if statements
return message
}, '')

I won't put the precise logic for how the plural element specifically is handled. But basically we just go through the props construct an ICU plural message from that. Which would look something like this:

We are having a sale all day today on {
numCategories,
plural,
one {something special}
other {# things}
}!

And Step 3 is done!

Handling nesting

Let's say we wanted our plural text to link to our sale page.

<p intl={locale}>
We are having a sale all day today on{' '}
<Link to="/on-sale">
<Plural
value={numCategories}
one="something special"
other="# things"
/>
!
</Link>
</p>

Fortunately because we use Stele we can use the Link from react-router or your design system easily!

What does Stele see with this?

const ast = {
openingTag: JSXOpeningTag('p'),
props: ObjectExpression([
ObjectMemberExpression(StringLiteral('intl'), Identifier('locale')),
]),
children: [
StringLiteral('We are having a sale all day today '),
JSXElement(
Identifier('Link'),
ObjectExpression(
MemberExpression(StringLiteral('to'), StringLiteral('/how-to'))
)
JSXElement(
Identifier('Plural'),
ObjectExpression(
MemberExpression(StringLiteral('one'), StringLiteral('something special'))
MemberExpression(StringLiteral('other'), StringLiteral('# things'))
)
)
)
StringLiteral('!'),
],
}

Now what we want to do, is keep the hierarchy intact. We do it with just a little bit of magic characters.

let messageCounter = 0
const extractMessage = (message, child) => {
/* stuff from above... */
if (isJSXElement(child)) {
/* the same stuff above */
if (isSteleElement(child)) {
} else {
messageCounter++
message = `<${messageCounter}>${jsxElement.children.reduce(
extractMessage,
'',
)}</${messageCounter}>`
}
}
// other if statements
return message
}
const icuMessage = jsxElement.children.reduce(extractMessage, '')

RECURSION! We basically, are repeating the same logic in the children as if it is a new message. We also wrap it with pseudo-HTML tags: <1></1>. In the end we get a message that looks like:

We are having a sale all day today on <1>{
numCategories,
plural,
one {something special}
other {# things}
}!</1>
© Patreon