Compatibility
Published
Localization is one of the few surfaces where a rename failure is visible to end users rather than to engineers. There is no exception, no blank screen and no console error -- there is a button in production with the word checkout.submit on it. The measurement below turns out to hinge on a single detail: whether your catalog is JavaScript or data.
What was measured
The sample holds two catalogs with byte-identical content. The first is written as an ordinary JavaScript object literal in the file, the way a small application usually starts. The second is produced by parsing a JSON string, standing in for the catalog file that a real localization setup ships alongside the bundle. Both have flat keys, one nested group two levels deep, and two messages containing interpolation placeholders.
Both are handed to the same translator function, which splits a dotted key path, walks the catalog, and returns a marker of the form !!key when a message cannot be found -- because that is what a user would actually see. There is also a plural selector reading a category dictionary keyed by one and other, and a missing-key report of the kind a continuous integration check would assert on.
In the base configuration everything was identical to the original on all five profiles measured. Message text is string data, and the string transforms change where a literal is stored rather than what it is, so nothing here is disturbed until member renaming is enabled.
The whole result is one line of difference
With a member pattern matching three of the catalog keys, the two catalogs diverged completely, and it is the cleanest safe-versus-unsafe pair this series has measured, because the two objects differ in exactly one respect.
The JavaScript object literal had its keys renamed. Every lookup against it then failed, and the translator returned its marker: !!greeting, !!cart, !!farewell. The nested group behaved the same way -- a pattern matching the path segments produced !!menu.file.open. Reading the catalog's own key list confirmed it directly: greeting,cart,farewell,menu became _0x1,_0x2,_0x3,menu.
The catalog parsed from JSON was untouched. Same keys, same lookups, same messages, on both targets. The obfuscator never opens a data string to rewrite key names inside it, so the catalog and the string keys used to look things up stayed in agreement.
The lookup keys were never the problem, which is worth being explicit about because it is the intuitive suspicion. Calls like t('greeting') pass a string literal, and string literals are not rename sites. The keys in the JSON catalog are also just characters in a string. The only thing that moved was the set of property names in the object literal, and that was enough.
One detail from the same arm is genuinely reassuring. A missing-key report -- the sort of check a build pipeline runs to catch untranslated strings -- correctly listed all three keys as missing after renaming. If you run that check against your built output rather than against your sources, it catches this before your users do.
The parameters break both catalogs, and that defeats the obvious fix
Moving your catalog into a JSON file is the natural conclusion to draw from the previous section, and it is worth drawing, but it does not finish the job. The next arm shows why.
Interpolation parameters are passed as an object: t('greeting', { name: 'Ada' }). The formatter reads that object using a name it extracts from the message string at run time, because the message says Hello, {name}! and the placeholder is what tells the formatter which property to look up. A member pattern matching name and count renamed the parameter object's keys, the formatter looked for properties that no longer existed, and every placeholder was left exactly as written.
Users saw Hello, {name}! and You have {count} items in your cart. Both catalogs were affected, including the JSON one, because the catalog was never the issue in this arm -- the parameter object is JavaScript regardless of where the messages live. Nothing was thrown, and every message was found successfully; only the substitution failed.
This is the same mechanism the Intl work on this site measured with formatter options, and it is the general shape worth internalising: an object whose keys are read by a name that lives in a string is a dictionary with an outside reader, even though every character of it is in your own source file.
Plural categories behave the same way and are easy to miss because the names are so short. The category dictionary is keyed by one and other, and the selector returns one of those words at run time. Renaming them produced !!item.one and !!item.other. The platform method itself fails loudly by comparison -- a pattern matching select threw a TypeError immediately, which is the merciful arm.
Where real setups sit between the two catalogs
The sample deliberately measures the two extremes, because that is what isolates the mechanism. Most real projects sit somewhere in between, and it is worth mapping the common arrangements onto the result.
A catalog imported from a .json file behaves like the data case. Bundlers typically inline that JSON as a parsed object literal, which sounds like it should turn it into the unsafe case, but the keys still originate outside your source and, more importantly, the mitigation is the same either way: keep member patterns off them. If your bundler does inline the catalog, the object literal it produces is a rename site like any other, so this is worth checking rather than assuming.
A catalog written as a JavaScript or TypeScript module -- export default { greeting: '...' } -- is squarely the unsafe case. This is a popular arrangement because it gives type checking over message keys, and the type safety is real, but it puts every message name into your source as a property name.
A catalog fetched at run time is the safest arrangement of all, since it never passes through the build. The trade-off is a network round trip before the first render, which is why so many projects inline the default locale and fetch the rest -- an arrangement that puts one locale in the unsafe category and the others in the safe one, which is worth knowing when only some of your languages misbehave.
Framework helpers do not change the analysis. Whether the lookup is a function call, a component, or a template directive, the question is the same one: is the message name a property of a JavaScript object in your build, or is it a string. The measurement above answers it for both.
What to do about it
Ship catalogs as data. This is good practice for reasons that have nothing to do with obfuscation -- it is what lets translators work without touching code, and what lets you load one locale instead of all of them -- and it happens to remove the larger half of this problem completely. A catalog that arrives as JSON is not a rename site.
Keep member patterns off your placeholder names. This is the half that surviving as data does not fix. Placeholder names tend to be short and generic, which is exactly the profile of a name that a broad pattern catches by accident, and name, count and value are three of the most common. If your patterns are anchored to something distinctive to your application, as this site recommends generally, you are already clear of it.
Run your missing-key check against the built output. Most localization setups already have one. Pointing it at the protected bundle rather than at the sources turns the catalog half of this into a build failure, which is where you want it.
The end-to-end check is one screenshot. Render a page in a non-default locale from a protected build and look at it. Raw keys mean the catalog was renamed; visible braces mean the parameters were. The two failure modes look different on screen, which makes this the rare case where the cheapest check is also the most complete one.
Frequently asked questions
Does obfuscation break translations?
Not in the base configuration, where the sample measured identical to the original on all five profiles. Message text is string data, and the string transforms change where a literal is stored rather than what it is. Translations only become a risk when member renaming is enabled, and then only for catalogs written as JavaScript objects and for interpolation parameter names.
Why does my application show raw message keys after obfuscation?
Because the catalog is a JavaScript object literal and member renaming rewrote its property names. The lookup keys are string literals and were never touched, so every lookup now misses and your translator returns its not-found marker. Reading the catalog's own key list confirms it: the message names are replaced by generated ones. A catalog loaded as JSON does not have this problem.
Is it safer to load translations from a JSON file?
For the catalog itself, yes, and the measurement is unambiguous: two catalogs with byte-identical content, one written as an object literal and one parsed from JSON, diverged completely under the same member pattern. The object literal returned not-found markers for every message and the JSON catalog was untouched. It does not fix the interpolation parameters, which are JavaScript wherever the messages live.
Why are placeholders like {name} showing up literally in my UI?
Because a member pattern renamed the keys of the parameter object. The formatter reads that object using the name it extracts from the message string at run time, so once the property is called something else the lookup misses and the placeholder is left as written. Nothing is thrown and the message itself is found correctly -- only the substitution fails, and it fails for a JSON catalog just as readily.
Do plural rules still work after obfuscation?
The selection does, but the dictionary it indexes may not. A category dictionary keyed by one and other is an ordinary object, so renaming those two names makes the lookup miss and the message resolve to a not-found marker. The platform method fails more obviously: a pattern matching select threw a TypeError immediately.
Are translation lookup keys renamed?
No. A call such as t('checkout.submit') passes a string literal, and string literals are not rename sites. The same is true of the keys inside a JSON catalog, which are characters in a string rather than property names in your code. Only property names in JavaScript objects move.
What is the fastest way to check a localized build?
Render one page in a non-default locale from a protected build and look at it. Raw message keys mean the catalog was renamed; visible braces around placeholder names mean the parameter object was. The two failure modes are visually distinct, so a single screenshot tells you which one you have, or that you have neither.
Related reading