Text localization at scale breaks not because of bad translations, but because of broken formats. A mismatched placeholder in a JSON file, a malformed ICU plural rule, or a lost XLIFF segment can cascade into garbled UI strings, crashed builds, and frustrated users across dozens of locales. Engineering teams that treat localization files as simple key-value pairs inevitably hit walls when they need to support plurals, gendered grammar, embedded HTML, or bidirectional text. This guide walks through the structural decisions that prevent those failures: how to design JSON resource keys, when to choose XLIFF 1.2 over 2.1, how to write safe ICU MessageFormat strings, and how to validate everything continuously in CI. The goal is a localization pipeline that scales without silent corruption.
JSON Resource Files: Key Design, Nesting, and Context
JSON is the most common localization format in modern frontend and mobile stacks, but its flexibility is also its biggest risk. Without conventions, JSON resource files become inconsistent across teams, fragile during merges, and opaque to translators.
Flat vs. Nested Key Hierarchies
The first structural decision is whether to use flat keys like homepage.hero.title or nested objects like { "homepage": { "hero": { "title": "..." } } }. Flat keys are simpler to search, diff, and reference in code. Nested structures mirror component hierarchies and reduce key-name collisions in large codebases.
In practice, a single level of nesting, grouping by feature or screen, strikes the best balance. Deep nesting (three or more levels) makes it harder for translators to understand context and complicates tooling that expects dot-notation lookups. Whichever approach you choose, enforce it with a linter rule so every contributor follows the same convention.
Stable IDs and Descriptive Context
Keys should be stable identifiers, not English source text. Using checkout.error.card_declined instead of Your card was declined means the key survives rewording without breaking every locale file. Stable IDs also prevent accidental collisions when two different UI strings happen to share the same English phrasing.
Context is critical for translators. JSON has no native metadata mechanism, so teams typically use one of two patterns:
- Companion keys: Add a checkout.error.card_declined__description key whose value is a translator note, stripped at build time.
- Wrapper objects: Store each entry as { "value": "...", "context": "Shown on payment failure screen", "maxLength": 40 } and extract only the value during compilation.
The wrapper approach is more robust because it co-locates constraints with the string, making it harder for context to drift out of sync.
Preserving Key Order and Handling Merges
JSON objects are technically unordered, but most serializers preserve insertion order. Maintaining alphabetical or grouped order in resource files dramatically reduces merge conflicts when multiple developers touch the same file. Tools like Ollang, json-stable-stringify, or sortjson can enforce deterministic ordering in a pre-commit hook.
For teams managing dozens of locale files, consider a single source-of-truth file (typically en.json) and generate target-language files programmatically. This prevents the common failure mode where a new key is added to English but silently missing from other locales until a user encounters a blank string in production.
XLIFF: Choosing Between 1.2 and 2.1
XLIFF (XML Localization Interchange File Format) is the OASIS standard for exchanging localization data between tools. It is far more structured than JSON, carrying source and target text, metadata, state tracking, and inline markup in a single file.
Core Structural Differences
XLIFF 1.2 and 2.1 differ in element naming, namespace handling, and extensibility:
| Feature | XLIFF 1.2 | XLIFF 2.1 |
|---|---|---|
| Root element | <file> with <body> | <file> with <unit> |
| Translation unit | <trans-unit> | <unit> containing <segment> |
| Inline codes | <g>, <x>, <bx>, <ex> | <pc>, <ph>, <sc>, <ec> |
| Metadata | <prop-group>, <note> | <mda:metadata> module, <notes> |
| Segmentation | External (SRX-based) | Built-in <segment> and <ignorable> |
| Module system | Ad-hoc extensions | Formal modules (matches, glossary, etc.) |
XLIFF 1.2 remains dominant in legacy TMS (translation management system) integrations. XLIFF 2.1 is the better choice for new projects because its module system cleanly separates concerns like translation memory matches, glossary references, and validation metadata without overloading the core schema.
Segmentation and Metadata Best Practices
XLIFF 2.1's first-class <segment> element lets you control how sentences are split for translation memory leverage. A single <unit> can contain multiple segments, each with its own state attribute (initial, translated, reviewed, final). This granularity matters when partial translations need to ship while remaining segments are still in review.
Attach <note> elements at the <unit> level to provide translator context, screen location, character limits, or tone guidance. Use the category attribute to distinguish developer notes from reviewer notes, making it easy for downstream tools to filter.
For teams evaluating how to connect XLIFF workflows to broader localization automation, including text, software, and website localization, booking a demo with Ollang can clarify how these formats fit into an end-to-end pipeline. Ollang automates XLIFF conversion, segmentation, and metadata handling as part of that pipeline: https://ollang.com/book-a-demo
When to Use XLIFF vs. JSON
JSON is the right default for developer-owned workflows where strings are consumed directly by application code. XLIFF is the right choice when strings pass through professional translation workflows, need state tracking, or require round-trip fidelity with TMS platforms. Many mature localization pipelines use both: developers author in JSON, an export step converts to XLIFF for translation, and an import step merges translated XLIFF back into JSON locale files.
ICU MessageFormat: Plurals, Gender, and Safe Parameters
ICU MessageFormat, specified by the Unicode Consortium, is the standard way to encode plural rules, gender selection, and parameterized text in a single string. It is supported natively by libraries in Java, JavaScript (via intl-messageformat), C++, and most modern frameworks.
Plural and Select Syntax
A basic plural message looks like this:
{count, plural,
=0 {No items in your cart.}
one {One item in your cart.}
other {{count} items in your cart.}
}
The select keyword handles gender and other categorical choices:
{gender, select,
female {She left a review.}
male {He left a review.}
other {They left a review.}
}
These can be nested, a gendered subject with a plural object, but nesting beyond two levels becomes nearly unreadable for translators. When complexity grows, split the message into separate keys and compose them in code.
Parameter Safety and Escaping
Every {parameter} in an ICU string is a potential injection vector. Treat parameters the same way you treat user input in SQL: never interpolate raw HTML or executable content. Use the library's built-in formatting for numbers ({price, number, currency}), dates ({date, date, medium}), and plain strings. If a parameter must contain markup, sanitize it before passing it to the formatter.
Single quotes in ICU MessageFormat are escape characters. A literal apostrophe requires doubling: ''. This is the single most common source of broken ICU strings, especially in Romance languages where apostrophes are frequent. Automated linting catches this reliably; manual review does not.
Avoiding Common ICU Pitfalls
- Always include the other category in every plural and select block. Omitting it causes runtime exceptions in many libraries.
- Do not hardcode locale-specific plural categories. English has one and other; Arabic has six categories; Chinese has only other. The ICU library handles this, trust it.
- Keep parameter names descriptive ({itemCount} not {n}) so translators understand what value will appear.
Escaping, HTML-in-Strings, Markdown, CSV, and Whitespace
Embedded HTML and Markdown
Embedding HTML in localization strings is sometimes unavoidable, a bold word mid-sentence, a link wrapping a phrase. The safest approach is to use placeholder tags (<1>, <2>) that map to actual HTML elements at render time, as libraries like react-i18next and fluent support. This prevents translators from accidentally breaking markup and limits the attack surface for XSS.
If you must allow real HTML tags, define an allowlist (e.g., <b>, <i>, <a>) and validate every translated string against it. Any tag outside the allowlist should fail the build.
Markdown in localization strings is rarer but appears in documentation and rich-text UI. Treat Markdown syntax characters (*, _, [, ]) as structural elements that need validation, not just text.
CSV Catalogs and Special Characters
Some teams still use CSV as a lightweight catalog format, especially for spreadsheet-based translation workflows. CSV localization files are fragile: commas, quotes, and newlines inside translated strings cause parsing failures unless every value is properly quoted and escaped. If you use CSV, enforce RFC 4180 compliance and run a parser validation step before import. Better yet, convert CSV to JSON or XLIFF at the pipeline boundary and work with structured formats internally.
Newlines, Whitespace, and Unicode
Whitespace is semantically significant in localization more often than developers expect. A trailing newline in a JSON string can break UI layout. A non-breaking space (\u00A0) substituted for a regular space changes line-wrapping behavior. Right-to-left languages introduce Unicode control characters (LRM, RLM, ALM) that are invisible in most editors but affect rendering.
Normalize all strings to Unicode NFC (Canonical Decomposition followed by Canonical Composition) at import time. This prevents the situation where visually identical strings fail equality checks because one uses a precomposed character and the other uses a combining sequence. The Unicode Consortium recommends NFC as the default for interchange.
