Compatibility

Does obfuscation break internationalization?

The question usually arrives with a specific fear attached: that a build step which rewrites strings will mangle the Japanese, corrupt the Arabic, or silently swap a locale key for an index. It is a reasonable fear and mostly an unfounded one, because translated content and program code are two different things and the transformations only ever operate on one of them. There is exactly one arrangement that genuinely breaks, and it is worth knowing precisely which one.

The distinction that answers most of the question

An obfuscator transforms a JavaScript program. It parses your source, rewrites it, and emits JavaScript. Anything that is not part of that source when the transformation runs is not part of what gets transformed.

That single fact resolves the majority of internationalization concerns, because in most applications the translations are not part of the source. They are locale files fetched over the network when the user picks a language, or when the application starts and reads a preference. Those files are data. They are downloaded after the protected bundle is already running, they are parsed by a library at run time, and the protection step never had an opportunity to touch them even if it had wanted to.

So the honest first answer to “will this break my translations” is a question back: are your translations in your bundle, or beside it? If they are beside it, you are done, and the rest of this article is a checklist you will not need. If they are in it, keep reading, because the answer is still mostly reassuring but it has edges.

When catalogues are compiled into the bundle

Plenty of applications do bundle their messages, and there are good reasons for it: one fewer network request, no flash of untranslated content, type checking over the keys, and a build that fails when a key is missing rather than a screen that renders a raw identifier. If that is your setup, your translated strings are string literals in JavaScript, and the string-handling options apply to them.

Three options are relevant, and none of them changes what a string evaluates to:

  • String encoding respells a literal as escape sequences. The documentation is explicit that the two forms are the same string as far as JavaScript is concerned: they compare equal, hash the same, and serialize the same. Only the bytes in the file change.
  • The string table lifts literals out of the statements that use them and puts them in one generated table, leaving an index at each use site. The values are unchanged and still present; what changes is that they no longer sit next to the code that explains them.
  • String encryption stores values encoded and rebuilds them through generated logic when the script runs. The reconstructed value is the value you wrote.

In every case the message your user sees is the message your translator wrote. What you are trading is file size and, for the encryption option, a small amount of work per string read — which is worth thinking about if your catalogue is large and you render a lot of text on first paint, and is unremarkable otherwise.

Non-ASCII output, and a small unexpected benefit

Here is the part that surprises people who expected corruption. The writer does not emit characters above the ASCII range directly into the output file. Any character above code point 255 is written as a four-digit unicode escape, and when string encoding is enabled, characters below 256 are additionally respelled as two-digit hex escapes.

The practical consequence is that a protected file containing Greek, Hebrew, Thai or Korean message text contains no bytes above 127 at all. It is pure ASCII. And that removes an entire category of deployment bug: the one where a server or a proxy declares the wrong character set, and text that was fine on your machine renders as replacement characters in production. An escape sequence has no encoding to get wrong. Nobody chooses an obfuscator for this, but if you ship translated content it is a genuine small win rather than a risk.

Characters outside the basic multilingual plane, which in practice means emoji and some historic scripts, are stored in JavaScript as two code units and are escaped as two consecutive sequences. That is a correct representation of the same string. If your own code does arithmetic on string length and assumes one character is one unit, it was already wrong before the protection step, and it is wrong in exactly the same way afterwards.

The one thing that genuinely breaks

Now the failure worth the article. It has nothing to do with strings and everything to do with how you reach the message.

Compare two ways of getting the same translated text:

  • t("checkout.total") — the key is a string literal. Every string transform preserves its value, so this keeps working under all of them.
  • messages.checkout.total — the key is member access. Member names are exactly what the member renaming option rewrites.

The second form is where things go wrong, and the mechanism is the one the member renaming documentation warns about directly: a renamed property is a different property, and anything that reads it by its original name stops working, usually at run time, on one code path, with no build error. The classic version of this involves a JSON boundary, and a translation catalogue is a JSON boundary. Your locale file was written by translators using the original key names. Parsing it produces an object with those names. Your code, after renaming, asks for something else. The lookup returns nothing, and depending on your library you get a blank string, the raw key echoed back, or a thrown error somewhere unhelpful.

Two things keep this from being a real problem in practice. First, member renaming is deliberately opt-in and is meant to be scoped: the recommended workflow is to mark genuinely private members with a naming convention and configure the rule so only those are eligible, rather than turning it loose on every property in the project. Second, where a catalogue really must be reached as an object, the reserved names setting takes regular expressions for names to preserve and keeps them out of the renaming pass entirely.

And if you have a choice, prefer lookup by string key. It is what every mainstream translation library encourages anyway, it survives every transform in the option set, and it keeps your message identifiers out of the part of the language that renaming operates on.

Keeping specific literals verbatim

Occasionally a literal needs to stay exactly where it is and exactly as written — a pattern another tool scans for, a marker some build step depends on, a key read by something outside your JavaScript. The reserved strings setting exists for that: it accepts regular expression patterns, and matching literals are kept out of the string table and the encoding pass rather than being lifted or respelled.

Reach for it narrowly. The point of the string options is that a bundle stops reading as an index of everything your application cares about, and a broad reservation pattern gives that back. A handful of precise patterns is a fine trade; a pattern that matches every message key is just the string options turned off with extra steps.

Extraction tooling, and the ordering rule again

Most internationalization workflows include an extraction step: a tool walks your source, finds every call to the translation function, and collects the keys and default messages into a catalogue for translators. Teams occasionally wire this into the release pipeline and then wonder why the extracted catalogue has been shrinking.

The reason is the rule that shows up in every article on this site about tools that read code. Extraction works by pattern-matching call sites and their literal arguments in source. After protection, the call sites have been renamed and rearranged and the literals may be indexes into a table, so the extractor finds a fraction of what it should — and it reports that fraction as a result rather than as an error.

Extraction is a source-time activity. Run it in the same phase as your linters, your type checker, your dependency scanner and your secret scanner: on plain source, in continuous integration, before anything protects anything. Then the pipeline has no ordering question left in it.

Things that are simply unaffected

It is worth listing these explicitly, because each one comes up:

  • Locale-aware formatting. Date, number and currency formatting go through browser APIs that receive the reconstructed string and the locale you passed. Nothing upstream changed what they receive.
  • Collation and sorting. Locale-aware comparison operates on string values, and string values are preserved.
  • Right-to-left layout. Direction comes from the characters, the markup and the stylesheet. A JavaScript transformation touches none of the three.
  • Pluralisation and interpolation patterns. A pattern is an ordinary string until your formatting library parses it, and it arrives at that library as authored.
  • Template-side translation calls. A translation call written inside a framework template is text in a template file, not JavaScript, until the framework compiles it — which is why what matters is where the protection step sits in your build, not what your templates look like.

A short checklist

  • Find out whether your catalogues ship inside the bundle or beside it. If beside, you are almost certainly finished.
  • Run message extraction before the protection step, alongside your other source-reading tools.
  • Prefer message lookup by string key over dotted access into a catalogue object.
  • If member renaming is enabled anywhere near your catalogues, scope it with a rule, or reserve the names.
  • Use reserved strings only for literals that genuinely must stay verbatim in place.
  • Run your end-to-end suite against the protected artifact, in more than one locale, including one that exercises a nested lookup and one that exercises the missing-key fallback.

That last point is the general form of all of this. Almost every surprise involving a protected build is caught by testing the artifact you actually ship rather than the source it came from, and internationalization is not a special case — it just has more visible symptoms when something does go wrong.

Frequently asked questions

Does obfuscation break translated text?

Not in the normal setup, because translated text is usually data rather than code. If your locale files are JSON fetched at runtime, a JavaScript obfuscator never sees them: it transforms the program, and a file downloaded later is not part of the program it transformed. The messages arrive exactly as authored. The cases worth checking are the ones where translations are compiled into the bundle as JavaScript, and even then the risk is narrower than most teams expect.

What happens to non-ASCII characters in string literals?

They are escaped rather than emitted directly. The writer converts any character above code point 255 into a four-digit unicode escape, and the string encoding option additionally respells characters below 256 as two-digit hex escapes. A string containing Japanese, Arabic or Cyrillic text therefore leaves the engine as pure ASCII escape sequences. This is a behaviour worth knowing because it is mildly helpful: an output file with no bytes above 127 in it cannot be corrupted by a server sending the wrong character set header, which is a real and irritating class of bug when shipping translated content.

Do emoji and other astral characters survive?

Yes. Characters outside the basic multilingual plane are stored in JavaScript as a pair of code units, and each unit is escaped separately, so a single emoji becomes two consecutive unicode escapes. That is a correct representation and it round-trips to the identical string. What you should not do is assume a character is one unit when writing your own length or truncation logic, but that is true of any JavaScript that handles user-facing text and has nothing to do with the protection step.

Does string encoding change how translated strings compare or sort?

No. An escaped literal and the literal it was written from are the same string as far as the language is concerned: they compare equal, hash the same and serialise the same. The documentation is explicit that only the bytes in the file change, not the program's behaviour. Locale-aware comparison and sorting go through the internationalisation APIs in the browser, which receive the reconstructed string and behave exactly as they did before.

What is the one thing that genuinely breaks?

Looking up a message by member access when the member names have been renamed. There is a real difference between calling a translate function with a string key and reaching into a catalogue object through a dotted path. A string key is a literal, and literals keep their values through every string transform. A dotted path is member access, and the member renaming option changes member names. If your translations arrive as parsed JSON, that data keeps the keys the translators wrote while your code now asks for renamed ones, and the lookup returns nothing at run time with no build error.

How do we avoid that member renaming problem?

Prefer lookup by string key, which is what every mainstream translation library does anyway, and reserve the names when you cannot. Member renaming is opt-in and is intended to be driven by an explicit rule or an identity list rather than applied to everything, so the recommended arrangement is to mark genuinely private members with a naming convention and let only those be eligible. Where a catalogue really is accessed as an object, the reserved names setting takes regular expressions for names to preserve and keeps them out of the renaming pass.

Are message keys and pattern strings affected by the string table?

Their values are preserved, but their location in the file changes, and there is a setting for the cases where that matters. Moving strings into a table lifts literals out of the statements that use them and leaves an index behind, while encoding respells them; neither changes what the string evaluates to. If you have literals that must remain verbatim in place, the reserved strings setting accepts regular expression patterns and keeps matching literals out of the string table and the encoding pass.

Does obfuscation interfere with message extraction tooling?

It does if you run it in the wrong order, and this is the same rule that applies to every tool that works by reading code. Extractors scan your source for calls to a translation function and collect the keys and default messages they find. Point one at a protected bundle and it finds fewer calls, or none, because the call sites and their literals have been rearranged. Extraction is a source-time activity: run it in the same phase as your linters and scanners, before the protection step, and the problem disappears permanently.

What about pluralisation and interpolation patterns?

They pass through unchanged, because they are ordinary strings until a library parses them. A pattern with placeholder syntax or plural categories inside it is a string literal to the engine, so it is subject to the same escaping and table treatment as any other literal and arrives at your formatting library byte for byte as authored. The formatting happens at run time in code that received the correct pattern, so the output is what it was before.

Do right-to-left languages need anything special?

No. Text direction is decided by the characters themselves and by your markup and stylesheet, none of which a JavaScript transformation touches. If a right-to-left layout was correct before protection it is correct after it. The one thing worth checking is any logic that flips layout based on the active locale, and you should check that the same way you check the rest of your application, which is by running the end-to-end suite against the protected build rather than only against source.

What should we actually test after enabling protection?

Load the application in each supported locale against the protected build and click through the flows that render translated content, then confirm the fallback path when a key is missing. If you use member renaming anywhere near your catalogues, test a locale that exercises a nested lookup specifically. Running the existing end-to-end suite against the protected artifact rather than against source is the general form of this advice, and it catches this class of problem along with every other transform-related surprise.

Related reading