
Always go the extra mile
I just spent three days hunting down what I thought was a simple internationalization bug.
Let me walk you through this debugging journey. It is not something that will teach you a super niche and clever bug, it is a quite common one but the morale on this story is what counts.
The Easy Part
First, some context. I got assigned a task to add Chinese support to our app.
Adding Simplified Chinese (zh-CN) and Traditional Chinese (zh-TW) was straightforward - just the usual i18n workflow:
- Add locales to
lingui.config.js
- Create the folder structure (
locales/zh-CN/
,locales/zh-TW/
) - Extract and translate strings
- Test with browser language settings
Everything worked perfectly. Standard stuff.
Then came the request for Hong Kong support.
The Problem
”Should be just as easy” I thought. Hong Kong users need Chinese too, but they use Traditional script like Taiwan. So I added zh-HK
following the same pattern.
I opened the browser and immediately saw the first alarm, it wasn’t working… “What the hell?, well, maybe some classic cache thing, reload server here and there, spin up the app again…” Still not working, “Crap, something is going on, lets go with some console.logs()“
Browser sends: zh-Hans-HK
App expects: zh-HK
Result: Fallback to English
The browser extension I was using to change the locale on the fly was sending zh-Hans-HK
(Simplified script) but I’d set up zh-HK
expecting Traditional. And why was the browser sending Simplified for Hong Kong anyway?
Turns out browser locale extensions and system settings can send different script variants than you expect. The browser extension was using proper BCP 47 format (zh-Hans-HK
= Chinese, Simplified script, Hong Kong region) but our app was using simplified codes (zh-HK
).
Just a format mismatch “Ha, got it, some googling always helps”.
The Quick Fix
My first instinct was to add a simple conversion layer:
// Convert browser codes to app codes
const browserToAppLocale = {
'zh-Hans-HK': 'zh-HK',
'zh-Hans-CN': 'zh-CN',
'zh-Hant-TW': 'zh-TW',
};
locale = browserToAppLocale[locale] || locale;
Six lines. Done. “Chinese users happy”
But then came the code review question from my CTO that changed everything: “Why not just use proper BCP 47 names for the folders and lingui.config instead of this conversions in the code?”
The “Proper” Solution
Fair point. The “right” way would be:
- Rename folders to match web standards
- Update config files
- Embrace BCP 47 throughout
So I did exactly that:
- ✅ Renamed
zh-HK
folder tozh-Hans-HK
- ✅ Updated
lingui.config.js
- ✅ Rebuilt everything
- ✅ Tested
Everything looked perfect. But it still fell back to English.
Browser: zh-Hans-HK
Available locales: ['en', 'zh-CN', 'zh-TW', 'zh-HK', ...] // Still zh-HK!
Wait, what? The folder was renamed but the available locales list still showed the old name.
Going Deeper - The extra mile
The conversion layer worked, and even though the review from my CTO was suposed to work, it didn’t, if I were to be working somewhere else, maybe this could have gone through, but my CTO insists to “always go deeper”, this didnt have a tight deadline, and something was clearly happening. So I did what I had to do, I followed all the flow of how locales were being loaded, took some time but… That’s when I discovered the available locales came from webpack scanning folders at build time:
const locales = fs
.readdirSync(path.resolve(process.cwd(), 'src/shared/locales'))
.filter((file) => {
return file.length === 2 || file.length === 5; // 👀 ⚠️
});
And there it was. The real villain.
The Dragon
This innocent filter was rejecting zh-Hans-HK
because it’s 10 characters long, not 2 or 5 !!!.
The logic:
- ✅
en
(2 chars) - include - ✅
zh-CN
(5 chars) - include - ❌
zh-Hans-HK
(10 chars) - reject silently
The Real Fix
The actual solution wasn’t about locale conversion at all:
// Old: Hardcoded assumptions
.filter((file) => {
return file.length === 2 || file.length === 5;
});
// New: Check what we actually want
.filter((file) => {
const fullPath = path.resolve(process.cwd(), 'src/shared/locales', file);
return fs.statSync(fullPath).isDirectory();
});
One line change. Problem ACTUALLY solved.
What I Learned
This whole experience taught me some things:
Question legacy assumptions. Check every single line that might be interacting with your changes, dont ever assume that because something has been working perfectly for years, it must still work.
Debug multiple layers. Modern apps have many layers. The bug was in build config, but symptoms appeared at runtime (silently).
Silent failures suck. The webpack filter failed with no errors, no warnings. Just missing locales. These are the worst bugs to hunt.
Final Thoughts
In the end, we went with the BCP 47 approach because it’s standards-compliant. But we also learned to look deeper when “simple” fixes don’t work.
The real lesson?
Sometimes the dragon you’re fighting isn’t the one you think you’re hunting
Mario - 2025 😅
Probably heard that on a movie or something, don’t quote me there eheh
I’d love to hear from you, you can contact me via x
Have you ever found a “simple” bug that turned out to be something completely different? The debugging stories that teach you the most are usually the ones that surprise you