Auto Translate - Every word to every language
We recently rolled out a new translation system across our applications. The goal was simple: if an app needs to support a new language, developers shouldn’t have to spend days wiring up translation files, managing keys, or updating components manually.
Instead, translations should happen automatically. This is the story of how we built that system, which… wasn’t simple at all!
The Problem
Over the years, we accumulated a large number of apps. Like most products, they were filled with buttons, labels, placeholders, tooltips, and messages written directly in English. Whenever a team wanted to support another language, the process was always the same:
- Find the strings
- Extract them
- Create translation keys
- Update the UI
- Maintain translation files forever
It worked, but nobody enjoyed doing it. We started wondering whether the build process could do most of that work for us. That idea eventually became wick-ui-i18n.
Build-Time Translation Extraction
At the center of the system is a Vite plugin built specifically for our internal component library, wick-ui. The plugin scans application code during the build, finds text inside supported components, extracts it automatically, and generates a translation dictionary.
Developers keep writing components normally. No translation keys. No manual extraction.
How It Works
The plugin runs during Vite’s transform phase. Before doing any expensive parsing, it performs a quick check. If a file doesn’t contain a wick-ui component, it gets skipped immediately. Files that pass the check are parsed into a Babel AST (Abstract Syntax Tree), and the plugin walks through the tree looking for translatable content.
Handling Different Types of Text
One of the first challenges was that text can appear in several different forms:
<WuButton>Hello</WuButton>
<WuButton>{'Hello'}</WuButton>
<WuButton>Hello {name}</WuButton>
<WuButton>{`Hello ${name}`}</WuButton>
<WuButton children="Hello" />
Did you know? The expressions inside
${}in JavaScript template literals are formally called quasis.
Each case produces a different AST structure, so they need different handling. Plain JSX text is straightforward. Static string expressions are straightforward too. Template literals are more interesting. For example:
{
`Hello ${name}, how are you doing?`;
}
Since part of the text is dynamic, it can’t become a single translation key. Instead, the plugin extracts the static portions and leaves runtime values untouched. The result looks like this:
<>
<WuTranslate __i18nKey="Hello" />
{name}
<WuTranslate __i18nKey=", how are you doing?" />
</>
(Note: In this output, WuTranslate acts as a React fragment. More on this later.)
The same idea applies to translatable props such as placeholder, title, and aria-label. For example:
<WuInput placeholder="Search..." />
Becomes:
<WuInput placeholder={wt("Search...")} />
The HTML Entity Surprise
One of the more unexpected bugs appeared after the initial rollout. A string containing & displayed correctly in one place but showed the literal text & somewhere else.
The root cause was Babel.
When Babel parses JSX text, it automatically decodes HTML entities. By the time the plugin sees the value, & has already become &. Using that decoded value as a translation key caused inconsistencies when the code was rewritten.
The fix was to inspect the original source text instead of Babel’s decoded value. Whenever an entity is found, the plugin preserves it exactly as written and only extracts the surrounding text. This means:
<WuButton>Hello & World</WuButton>
Becomes:
<WuButton>
<WuTranslate __i18nKey="Hello" />
&
<WuTranslate __i18nKey="World" />
</WuButton>
The entity remains untouched while the surrounding text stays translatable.
The wt() Helper
Not every string lives inside JSX. Sometimes translations are needed in configuration objects, event handlers, utility functions, or hooks. For those situations, we added wt():
placeholder={wt('Enter your name')}
The plugin records these keys during the build process but leaves the code itself unchanged. From a developer’s perspective, it’s just another function call.
However, a standard function is not reactive. To solve this, we also introduced the useWt() hook to make text reactive when translations change dynamically.
Generated Output
As files are processed, the plugin builds a dictionary of translation keys. At the end of the build, that dictionary is written to wick-ui-i18n.json.
During development, Vite serves this file dynamically so newly added keys appear immediately without requiring a rebuild. The plugin also injects any required imports automatically.
Runtime Translation
The build step only creates the dictionary. Runtime translation is handled by wick-ui-lib.
WuTranslateProvider
Applications wrap themselves with WuTranslateProvider:
<WuTranslateProvider defaultLocale="es_LA">
<App />
</WuTranslateProvider>
When the app starts—or when the locale changes—the provider loads the generated dictionary. For English, that’s all that’s required.
For any other language, the provider sends the dictionary to our translation service and receives translated values in return. If the translation fails, the system falls back to the original English strings instead of breaking the UI.
With the hook, we can change the language dynamically. On change, we call the backend, sending the JSON file with the target language code, and it instantly returns the translation for the respective keys. The UI update is instantaneous. Although the performance tab might show multiple renders, they are lightweight text updates—so no worries!
WuTranslate
Text extracted by the plugin is rewritten into:
<WuTranslate __i18nKey="Hello" />
The component simply looks up the translated value and falls back to the original key when a translation is missing. This component actually replaces the text with a React Fragment. We could have used a span or another HTML node, but we chose <Fragment> so the plugin wouldn’t inject any unnecessary DOM nodes. It also makes debugging significantly cleaner.
Implementation Note: We used
magic-stringduring the compilation phase to ensure that original line numbers persist for accurate debugging.
wt() and useWt()
For non-React code, wt() reads from a shared dictionary store and returns the translated value when available. When reactivity is required inside components, useWt() can be used instead. This allows us to fully support translations inside plain .ts files (like constant arrays we want to loop through).
Translation Management
Translations are managed through the Admin2 dashboard. The interface is split into two primary backend sections:
1. Automatic Translation Generation
Not every translation exists when an application first requests it. When the translation API receives a dictionary, any missing keys for the target language are automatically queued for processing.
A background service batches these missing entries and sends them through our AI translation pipeline, powered by our internal AI infrastructure and Google Translate. The generated translations are then stored in the database for future use.
While a translation is being generated, the API simply falls back to the original English text, ensuring the UI remains fully functional. Once generated, future requests are served directly from storage.
2. Translations & Languages Workflow
- Translations Section: This is where translation entries are reviewed and edited. Users can:
- Search translation keys
- Edit translations for individual languages
- Identify missing translations
- Save only modified values
- Import translations in bulk
- Test translations directly against the API
- Languages Section: This section manages supported languages. Each language stores metadata such as:
- Language code & Display name
- AI router code
- Character set & RTL (Right-to-Left) configuration
- Auto-translation support
The system currently supports ~200 languages, and the entire workflow—from key discovery to translation generation and storage—is fully automated.
End-to-End Flow
Here is exactly what the completed developer and user workflow looks like:
[Developer writes raw JSX]
│
▼
1. <WuButton>Save changes</WuButton>
│
▼ (Vite Build Phase via Babel AST)
2. Plugin extracts text to `wick-ui-i18n.json`
Component is rewritten to:
<WuButton><WuTranslate __i18nKey="Save changes" /></WuButton>
│
▼ (Translations managed/automated through Orion)
3. User opens the application in French
│
▼ (WuTranslateProvider fetches keys)
4. UI instantly renders:
"Enregistrer les modifications"
The developer never created a translation key, never maintained a translation file, and never added any i18n plumbing. They simply wrote a button.