Stele - Techdebt
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.