(inspired by Rich Harris's "Rethinking Reactivity")
🚀 TL;DR: wuchale
is a blazing-fast, zero-wrapper i18n solution for Svelte that extracts and compiles translations at build time. It avoids string keys, has minimal runtime cost, plays perfectly with HMR, and even supports Gemini-powered auto translation. Think LinguiJS, but leaner, faster, and tailored for Svelte.
A few years back, I needed to add i18n to my fairly large application with Preact. I evaluated the existing tools and the most common method was to write getters with keys for the actual text fragments which are located in separate language specific files.
<p>{t('page.home.greeting')}</p>
This works, and it "might" even be fast, but I believe it's fundamentally flawed. Text should live where it belongs: inside your markup, not behind opaque keys. Then I discovered LinguiJS. It was relatively new back then, but it introduced a refreshing idea. Instead of the above method, I would write:
<p><Trans>Hello</Trans></p>
The fragments are right there in the code. It auto extracts them using a command. This was much better and I migrated the whole application.
Leaving Preact, entering Svelte
Recently, I got some free time and wanted to move away from Preact to Svelte. But LinguiJS didn’t officially support Svelte. There was an adapter, but to me, it didn't feel like the real deal; it lacked the completeness and elegance of the original. Also I looked at the dependencies and apparently LinguiJS has grown to require more than 200! That was the final straw. I decided not to bring LinguiJS into my Svelte migration. However, the officially supported options for Svelte, like svelte-i18n
and paraglide
, relied on the same key-based approach I was trying to avoid.
Why does i18n have to be this hard?
It then occurred to me, it doesn't.
So I started playing with the svelte compiler and started to develop something that completely replaced my need for both key-based and wrapper-based methods. The design goals are simple:
-
You should just write your markup and it should be translated
<p>Hello</p>
It should have no compromise on performance.
Weeks of full time dedication later, it grew from a concept into a full open source package that you can use today.
wuchale
wuchale
is inspired by LinguiJS and built with many of the same principles, but with:
- zero wrappers
- smaller bundles
- blazing fast (TM) performance
- no dependency bloat
More specifically, it's a vite
plugin that pre-transforms your code and in the process also does other things like extracting the text fragments, and compiling the translated ones. And it can handle anything you can throw at it, because it only does AST based analysis of your code.
How does it work?
Let's pick the simple "Hello" example above. wuchale
does the following:
-
Convert your code into this:
<p>{wuchaleTrans(0)}</p>
-
Extract the text into a per language PO file (like LinguiJS) that you can exchange with translators:
#: src/file.svelte msgid "Hello" msgstr ""
-
Translator fills the translated content:
#: src/file.svelte msgid "Hello" msgstr "Hola"
-
Compile the translated fragment into an array:
export default ["Hola"]
Yes, the argument 0
in the call wuchaleTrans(0)
is actually the array index! Because wuchale
controls the entire pipeline, it can avoid string keys altogether and still have deterministic output using arrays. This makes it output the smallest possible bundle size for the compiled fragments, even smaller than what LinguiJS produces, because LinguiJS uses string keys. And vite
can further minify the call into something like w(0)
to make the transformed code even smaller than the hand written t('page.home.greeting')
! All of this makes it the best at producing small bundle sizes for performance.
So what happens if a translation is missing, will the user just see an index number (As LinguiJS just shows the meaningless string keys when there is no translated fragment)? Answer is no. wuchale
is smart enough to fall back to the source language (English in most cases) when the fragment is not yet translated.
Complex content? Handled beautifully
Glad you asked! Sometimes, we have to mix text content with variables and markup. And wuchale
does work in the most intuitive way. It goes out of its way to preserve the text fragments that you expect to be together, no matter the boundaries between them.
Let's say you have this:
<p>Hello {username}, welcome!</p>
This would be transformed into:
<p>{wuchaleTrans(0, [username])}</p>
And extracted to the PO file as:
#: src/file.svelte
msgid "Hello {0}, welcome!"
msgstr ""
And during runtime, it performs as fast as possible because it doesn't even parse and replace the placeholder. All of that heavy work is done during compile time. The compiled form of this (for English) would be
export default [
[ 'Hello ', 0, ', welcome!' ]
]
What the runtime has to do is just loop over the array and concatenate the strings and the arguments at the indices specified as numbers.
The most complex scenarios involve text mixed with nested markup:
<p>Hello {username}, <b>welcome <i>home</i></b>!</p>
We don't want home
to be extracted separately, but as part of the whole message. And wuchale
does that! It is extracted as:
Hello {0}, <0>welcome <0>home</0></0>!
And the translators have the freedom to rearrange the markup as the language dictates using the placeholders.
Convenience: Dev mode HMR
wuchale
plays nicely with Hot Module Replacement (HMR). It integrates deeply with Vite's HMR API and Svelte's reactivity. That means:
- When you update source
.svelte
files → translations are extracted, and.po
files are updated - When you update
.po
files → the browser receives live updates
All of this happens without a full page reload.
Bonus: Gemini-based auto translation
Having access to the whole process allows for more features and one is automatic translation. wuchale
can use your Gemini API key if it finds it as an environment variable, and auto translates your content. It's not a substitute for human quality; but it gets you 80% of the way instantly.
Together with support for seamless HMR, this enables you to work in your source language in the code and see the updates in the browser in another language in real-time!
Try it out!
- GitHub: github.com/K1DV5/wuchale
- NPM: https://www.npmjs.com/package/wuchale
- StackBlitz examples:
If you try it, I'd love your feedback. And if you find it useful, consider starring the repo ⭐
P.S. What's next? React support, perhaps. Got ideas? I'd love to hear them.