Shipping seven languages and showing everyone English
My VS Code extension had shipped translations for seven languages since October. Nine months later I found out that six of those bundles had never been displayed to a single user — and that my i18n test suite had been asserting through the bug the whole time.
- vs code
- extensions
- i18n
- testing
My minifier extension has shipped translations for seven languages since v1.1.0 in October 2025: English, Spanish, French, German, Portuguese, Japanese and Chinese Simplified. Seven JSON bundles, each one reviewed, each one in the package.
Six of them had never been shown to anybody.
Every runtime message — every validation error, every success notification, every exception dialog — rendered in English no matter what display language VS Code was running. For nine months. The files were on disk, correct, and unreachable.
The part that did work
This is why it survived nine months: half the localization was fine.
VS Code has two localization surfaces. Static contributions — command titles, setting descriptions, menu labels — are declared in package.json with %placeholder% syntax and resolved from package.nls.json, with package.nls.es.json and friends picked up automatically. That's platform machinery, I set it up on day two back in 2024, and it has always worked. Open the command palette in Spanish and the commands are in Spanish.
Runtime strings — anything you pass to showInformationMessage or showErrorMessage — are a completely separate system, vscode.l10n, with its own bundle format. That's the half I got wrong.
So the extension presented as localized. The commands were translated, the settings were translated, and then the moment it actually told you something, it switched to English. If you weren't specifically looking for it, it read like a partially-translated extension rather than a broken one.
The cause was a helper I wrote myself
export function t(key: string, ...args: (string | number | boolean)[]): string {
// Never calls vscode.l10n.t() — always reads the English-only in-memory bundle
let message = l10nBundle[key] || key;
args.forEach((arg, index) => {
message = message.replace(new RegExp(`\\{${index}\\}`, 'g'), String(arg));
});
return message;
}
At activation it read one file — bundle.l10n.json, the English bundle — into memory, and every call site looked up a symbolic key in it (minificationService.error.jsLocal, that sort of thing). It never called vscode.l10n.t(). It never consulted the user's locale, because it had no idea locales existed.
I wrote a bundle loader. VS Code already had one. That's the entire bug.
Two decisions compounded into it. The first was writing infrastructure instead of reading the official l10n sample, which is about forty lines and does this correctly. The second was symbolic keys: minificationService.error.jsLocal gives you no signal when lookup fails. If a key is missing you get the key back, and a dotted string in a notification looks enough like a deliberate identifier that it doesn't scream.
Why the tests were green
The extension had an i18n test suite. Twenty-plus tests. All passing, the entire time.
They asserted through t() — the same broken helper. The tests were asking the bug whether the bug was working, and the bug said yes. If your assertion and your implementation share a code path, you have written a tautology with good syntax and a green checkmark.
There's a second, quieter reason, and it generalises further than the first: CI ran in exactly one locale. English. And under English, a correct implementation and a completely broken one produce byte-identical output. The only environment where the bug is invisible is the only environment I ever tested in.
The evidence was already in my tracker
Nobody reported this. Not once in nine months.
But I had the data. The bug report template asks for VS Code Display Language, and reports came in with it filled out. #168, filed against a completely different bug, says Japanese (ja) — a user running my extension in Japanese, filing a careful report about activation events, while every message the extension had ever shown them was in English. They didn't mention it. Why would they? Half-English tooling is unremarkable if you work in a language other than English.
I built a field into the form that told me non-English users existed, collected the answers for nine months, and never once read them as a group.
The fix
The canonical pattern is the opposite of what I built. You don't invent keys — you pass the English source string as the key:
t("File type '{0}' is not supported for minification.", fileType)
Under English, vscode.l10n.t() returns that string as-is: the default locale becomes a no-op with no bundle at all. Under any other locale it looks the string up in bundle.l10n.<locale>.json. So l10nHelper.ts collapsed to a one-line delegate:
export function t(message: string, ...args: (string | number | boolean)[]): string {
return vscode.l10n.t(message, ...args);
}
Then: regenerate all six non-English bundles so their keys are the English source strings (the translations themselves carried over verbatim — only the keys changed), and delete bundle.l10n.en.json entirely. English lives in the source code now. There is nothing to keep in sync.
The activation-time preload went away with it, and that turned out to matter for a different reason: activate() had been awaiting the bundle load before registering its commands. A user who clicked a menu item fast enough landed in that window and got command 'extension.minifyInNewFile' not found — one of several independent causes of that same message. A localization mistake had quietly become an activation bug.
Testing something you can't read
The new suite runs VS Code under a real locale. .vscode-test.mjs forwards VSCODE_LOCALE to --locale, so:
VSCODE_LOCALE=es npm test # Spanish
VSCODE_LOCALE=qps-ploc npm test # pseudo-locale
qps-ploc is the one worth knowing about. It's VS Code's official pseudo language pack: every localized string renders with accents and markers, so anything that comes out in plain English is a string that never went through the l10n layer. You don't need to speak the language to see the bug — that's the point. Unlocalized text becomes visually obvious to a monolingual maintainer.
Three assertions now guard the regression:
t()returns real interpolated text, never a raw key.{0}/{1}placeholders survive end-to-end.- Under a non-English locale,
vscode.l10n.bundlematches the shippedbundle.l10n.<locale>.json— and fails if a "translation" turns out to be a copy of English.
Confirmation from someone who could actually tell
I shipped it in v1.3.3 and asked the Japanese-locale reporter from #168 to smoke-test it, since they were the one person I knew was running a non-English VS Code. The next day:
Confirmed working on v1.3.3 with Japanese locale (macOS 26.5.2).
That is the first verified evidence in the project's history that a non-English user got a build that spoke to them in their language. It arrived nine months after I announced seven-language support in a changelog.
What I took from it
Use the platform's API before writing your own. The sample existed, the API existed, and I wrote a worse version of both because I didn't look. Every line of infrastructure you write instead of reading is a line you now maintain and test alone.
English-as-key is not a style preference. It makes the default locale a no-op, makes a failed lookup return readable English instead of a dotted identifier, and makes the call site say what it displays. Symbolic keys hide their own failures.
A test that shares a helper with the code under test proves nothing. Green for nine months across a feature that never worked once. The assertion has to reach the user-visible surface by a different path than the implementation does.
CI runs in one locale; your users don't. Any bug whose signature is "wrong language" is invisible in the only environment most projects ever test. A pseudo-locale run costs one environment variable and makes it visible.
Shipping a translation is not delivering it. Between a reviewed JSON file and a message on someone's screen there's a bundle format, a loader, an API, an activation path and a locale — and the changelog entry only proves the first one exists.
Read your own bug reports as a dataset. The display-language field had been answering a question I never asked. The information you need is often already collected, sitting in a form field nobody aggregates.
The extension is on the Marketplace as JS & CSS Minifier Tool and Compressor, and it now genuinely speaks all seven languages it advertises. Source at miguelcolmenares/css-js-minifier.
If you run VS Code in something other than English, I'd particularly like to hear from you: a message that reads awkwardly, a translation that's technically correct but wrong for the context, a string that's still stubbornly English. Open an issue — and if you want to add an eighth language, it's one JSON file plus a line in the docs, and I'll help with the wiring.