Most localization bugs don't originate in translation. They originate in how engineers structure strings, choose file formats, and handle edge cases like plurals, gender agreement, and bidirectional text. A misnamed placeholder silently breaks a Turkish build. A naive concatenation produces nonsensical word order in Japanese. A missing bidi mark flips an entire address in Arabic. These are engineering problems, not linguistic ones, and they require engineering solutions.
This guide covers the core technical decisions that determine whether your strings are robust, testable, and locale-correct: ICU MessageFormat syntax, file format tradeoffs between XLIFF, JSON, and YAML, segmentation and context strategies, text expansion handling, and the specific mechanics of right-to-left rendering. After reading, you'll have concrete patterns, schemas, and CI checks you can apply immediately.
ICU MessageFormat Essentials
ICU MessageFormat is the industry standard for encoding locale-sensitive string logic, plurals, gender selection, and conditional branches, directly into translatable messages. Maintained by the Unicode Consortium, it separates linguistic rules from application code, which means translators handle grammar while developers handle variables.
Plural, Gender, and Select Rules
English has two plural forms (singular and plural). Arabic has six. Polish has four, with complex rules governing which numeral triggers which form. Hardcoding if count == 1 checks in application code is fragile and fails the moment you add a language with different plural categories.
ICU MessageFormat solves this with plural, select, and selectordinal keywords:
{count, plural,
=0 {No messages}
one {# message}
other {# messages}
}
The # symbol is replaced by the numeric value. The plural categories (zero, one, two, few, many, other) are defined by CLDR plural rules, not by the developer. Every locale must include other as a fallback.
Gender selection works similarly:
{gender, select,
female {{name} updated her profile}
male {{name} updated his profile}
other {{name} updated their profile}
}
Nested constructs are valid, you can nest a plural inside a select, but deeply nested messages become difficult for translators to parse. As a rule, limit nesting to two levels and split more complex logic into separate strings.
Placeholder Naming and Safety
Placeholders are the contract between your code and your translators. Poorly named or undocumented placeholders cause mistranslations and runtime crashes.
Follow these conventions:
- Use descriptive names, not positional indices. {userName} is unambiguous; {0} is not.
- Use camelCase consistently across your codebase. Mixed conventions (user_name in one file, userName in another) create confusion.
- Never embed HTML or code logic inside placeholders. If you need formatting, use ICU's built-in number and date skeletons: {deadline, date, medium}.
- Document every placeholder with a developer comment explaining its type and expected content.
A placeholder naming schema might look like this:
| Pattern | Example | Use |
|---|---|---|
| {entityAction} | {fileUploaded} | Event descriptions |
| {count} | {itemCount} | Numeric values for plurals |
| {name} | {userName} | Proper nouns / display names |
| {date, date, skeleton} | {expiryDate, date, yMMMd} | Formatted dates |
Validation and Linting Strategies
ICU syntax errors, an unclosed brace, a missing other clause, should never reach production. Catch them in CI.
Several tools parse and validate ICU messages:
- @formatjs/cli for JavaScript/TypeScript projects can extract and validate ICU messages at build time.
- messageformat-validator checks structural correctness and enforces that all plural categories required by target locales are present.
- Custom lint rules can enforce your naming conventions, flag positional arguments, and reject concatenation patterns.
Ollang can centralize these checks in your localization pipeline and surface errors as part of pull-request feedback, making it easier to enforce ICU correctness across teams. You can see how this integrates into CI by booking a guided walkthrough: Schedule an Ollang demo.
A minimal CI check should:
- Parse every ICU string and reject syntax errors.
- Verify that all required CLDR plural categories exist for each target locale.
- Confirm that placeholder names in translations match the source exactly.
- Flag strings exceeding a maximum nesting depth.
File Format Comparison: XLIFF, JSON, and YAML
Your choice of localization file format affects tooling compatibility, translator context, and how much metadata you can attach to each string. There is no universally correct answer, the right format depends on your stack, your TMS, and how much contextual data you need to ship alongside your strings.
XLIFF 1.2 vs XLIFF 2.1 Feature Matrix
XLIFF (XML Localization Interchange File Format) is an OASIS standard designed specifically for localization interchange. It is the most feature-rich option and the lingua franca of professional translation tools.
| Feature | XLIFF 1.2 | XLIFF 2.1 |
|---|---|---|
| Inline markup (bold, links) | <g>, <x/>, <bx/> tags | Simplified <pc>, <ph>, <sc>/<ec> |
| Segmentation | External or embedded | Native <segment> and <ignorable> |
| Notes / comments | <note> element | <note> with appliesTo attribute |
| Metadata extensibility | Limited namespace support | First-class <mda:metadata> module |
| Validation module | None | Built-in validation module |
| Tool adoption | Universal TMS support | Growing; some tools still 1.2-only |
XLIFF 2.1 is the better technical specification, cleaner inline handling, native segmentation, and modular extensibility. However, XLIFF 1.2 remains more widely supported across legacy TMS platforms. If you're starting a new project, prefer 2.1. If you're integrating with an existing pipeline, verify your toolchain's support first.
JSON and YAML Patterns for Developers
For web and mobile projects, JSON is the most common localization format due to its simplicity and native support in JavaScript ecosystems. YAML is popular in Ruby on Rails and some game engines.
A flat JSON structure:
{
"nav.home": "Home",
"nav.settings": "Settings",
"error.notFound": "Page not found"
}
A nested JSON structure:
{
"nav": {
"home": "Home",
"settings": "Settings"
},
"error": {
"notFound": "Page not found"
}
}
Flat structures are easier to search and diff. Nested structures group related strings logically but can cause merge conflicts when multiple developers edit the same namespace. Pick one convention and enforce it project-wide.
YAML adds readability but introduces indentation-sensitivity bugs and inconsistent multiline string handling across parsers. If your stack doesn't require YAML, JSON is the safer choice.
Keys vs Source-as-Key: When to Use Each
Two schools of thought exist for identifying translatable strings:
- Semantic keys (error.auth.invalidPassword) decouple the identifier from the English text. Changing the source copy doesn't break references. This is the preferred approach for large, long-lived projects where strings are reused across components.
- Source-as-key ("Invalid password" is both the key and the English value) reduces boilerplate and is popular in frameworks like react-intl and gettext. The tradeoff: changing the English text means updating every translation file's key, and identical English strings with different meanings (e.g., "Save" as a verb vs. "Save" as a noun) collide.
| Criterion | Semantic Keys | Source-as-Key |
|---|---|---|
| Rename safety | High | Low |
| Disambiguation | Built-in via key | Requires manual context |
| Developer ergonomics | More boilerplate | Less boilerplate |
| Best for | Large apps, shared components | Small apps, rapid prototyping |
For enterprise-scale localization, especially when working with professional translators and a TMS, semantic keys with mandatory developer comments provide the most reliable foundation.
Segmentation, Context, and Developer Comments
Translators working without context make mistakes. Context is not optional; it is a direct input to translation quality.
SRX Segmentation Rules
Segmentation is the process of splitting text into translatable units, typically sentences. The SRX (Segmentation Rules eXchange) standard defines language-specific rules for where to break text.
Default segmentation breaks at sentence boundaries (periods, exclamation marks, question marks). But abbreviations like "Dr.", "U.S.A.", and "e.g." create false positives. SRX rules define exceptions:
<rule break="no">
<beforebreak>\b(Dr|Mr|Mrs|Ms|Prof)\.</beforebreak>
<afterbreak>\s</afterbreak>
</rule>
Most TMS platforms apply SRX rules automatically, but engineers should be aware of segmentation when:
- Strings contain multiple sentences (which should generally be split into separate keys).
- UI text includes abbreviations or domain-specific notation that could trigger false breaks.
- Content is pre-segmented in code, which can conflict with TMS segmentation.
The safest practice: one sentence per string key. This gives translators maximum flexibility to reorder words without being constrained by adjacent sentences.
Developer Comments and Screenshot Linking
A developer comment should answer three questions: Where does this string appear? What do the placeholders contain? Are there length or formatting constraints?
In JSON, comments can be added via a convention like _comment suffixes or a companion metadata file. In XLIFF, the <note> element is purpose-built:
<trans-unit id="btn.save">
<source>Save</source>
<note from="developer">Button label in the document editor toolbar. Max 10 characters. "Save" as a verb (action), not a noun.</note>
</trans-unit>
Screenshot linking takes context further. Attach a screenshot URL or reference ID to each string so translators see the exact UI where the text appears. XLIFF 2.1's metadata module supports this natively. For JSON-based workflows, tools like Lokalise and Phrase allow screenshot uploads tied to specific keys.
Ollang and similar platforms that handle the full localization pipeline, from string extraction to quality review, can integrate context delivery directly into translation workflows, reducing the back-and-forth that slows down release cycles.
