Stele - Techdebt

Components
HomeHow Stele Works
How To
LicenseTechdebtJSX AST FlavorICU Message Format ParsingStatefulness and Initialization

Tech Debt

Here is an assortment of known technical debt that should be addressed.

JSX AST Flavor

Stele currently detects, processes, and possibly changes JSX after the JSX has been compiled to React.createElement calls. Here are some reasons why that is not ideal, in no particular order:

  • In Babel, plugin order matters. This means Stele must be run after JSX transforms have occurred, and no sooner.
  • Since both JSX usages and dunder function usages end up as call expressions, it becomes harder to disentangle the two parts visually and conceptually.
  • Within the React ecosystem, there is a planned migration that deprecates React.createElement. Although not an immediate concern, this means the current architecture is not future-proof.

Removing this tech debt involves rewriting Stele to deal with JSX nodes directly in tasks such as extractability detection, ICU string extraction, and tree replacement.

For TypeScript users, this means that the compiler option jsx will have to be set to "preserve" instead of "react", further making it necessary to enable the @babel/plugin-transform-react-jsx plugin.

Decoupling

Gauging the kind of extraction (extractabilityOfExpression) applicable and performing the actual extraction are two phases that are deliberately decoupled at the moment. This decoupling is unnecessary: when a function call is recognized as a dunder usage, a message is always extracted from that call. The same applies to JSX expressions. Continuing this decoupling creates extra work for the extraction phase itself, work that has already been performed by the gauging phase.

Knowing this, the extractability module should be collapsed into the extractors module, resulting in two primary functions, tentatively named extractFromJSXIfValid and extractFromCallIfValid.

ICU Message Format Parsing

A key feature of Stele is automatically creating ICU messages from JSX chunks. This is achieved by representing nested JSX elements as XML-like tags. As long as the translated ICU message's tags form the same tree shape as the source ICU message, Stele can reliably combine the original JSX tree with the translated ICU message into a translated JSX tree. This, in turn, is achieved by parsing an ICU message into an AST.

The library for parsing an ICU message, intl-messageformat-parser, refers to each node in the AST as an "element". This is because it is common for an ICU message to have multiple root nodes and a depth of one. The only time a tree can be deeper then one level is when a {select} format, a {plural} format, or a {selectordinal} format is used.

As intl-messageformat-parser evolves, the AST has become more sophisticated. For example, version 3.6.0 added a PoundElement to support escaping the # sign inside of a {plural}. This meant that Stele needed to be updated to support that too.

In order to understand the XML-like tags inside of an ICU message, Stele performs parsing and book-keeping of its own. The bad news is that this is fragile and complicates both the extraction process and the replacement process. The good news is that since version 4.0.0, intl-messageformat-parser can parse XML-like tags, which is represented by TagElement.

Solving this tech debt entails upgrading intl-messageformat-parser to version 4.0.0 or newer, as well as migrating Stele's tag handling to take advantage of TagElement.

Statefulness and Initialization

Stele performs two main functions: extraction of messages in a codebase, and replacement of messages when building non-English bundles of the codebase. These two functions exhibit very different state change patterns:

  • Extraction gradually builds up multiple messages as more and more files are processed, culminating in a final exportation step when all files are processed away. With an AST visitor function as the focal point, extraction is very stateful.
  • Replacement, in contrast, loads an entire catalog on launch, and uses that information as a lookup table for replacing text. With the visitor function as the focal point, replacement is more or less stateless.

Singleton

All instantiated instances of Stele share the same catalog. This effectively makes Stele a module-level singleton, which means once Stele is require'd, the only way to get back to an initial state is to call the exported function resetMessages.

This makes it harder to do various tasks, from more obvious ones like having multiple instances of Stele in the same Node process, to less obvious ones like unit-testing Stele and managing the lifecycle of instantiating the catalog.

Splitting

Instead of keeping Stele as a single plug-in that runs in either extraction mode or replacement mode, we could split Stele into two modules, one specifically for extraction, the other for replacement.

For the purposes of extraction, Stele does not even need to run as a Babel plugin. Babel's own API is rich enough such that Stele could effectively use it to parse a set of source files without the need of any transforms / emit.

© Patreon