Preserving Formats and Placeholders: Safe API Integration Guide
A safe-integration guide for translation APIs: protecting placeholders, ICU plural rules, and HTML tags from silent corruption, with validation layers that keep translated strings from breaking your app in production.

When your translation API silently drops a {userName} placeholder, the user sees a broken string. When it mangles an ICU plural rule, your app crashes in production. When it strips an HTML tag mid-sentence, your rendered page breaks. These failures are preventable, but only if your integration treats format preservation as a first-class concern rather than an afterthought. This guide walks through the practical techniques, placeholder locking, tag handling, escaping strategies, and automated validation, that keep structured content intact as it moves through translation APIs at scale. Whether you're localizing UI strings, templated emails, or documentation with embedded markup, the patterns here will help you ship confidently in every target language.
If you're evaluating how to handle this across text, software, and document localization workflows, explore how Ollang's API layer manages format integrity end to end.
Understanding Placeholders and Structured Content
What Are Placeholders in Localizable Strings?
Placeholders are tokens embedded in translatable text that get replaced at runtime with dynamic values, user names, counts, dates, currency amounts. They appear in many forms:
- Numbered: {0}, {1}, %1$s
- Named: {userName}, {{orderTotal}}, %{count}
- Positional printf-style: %s, %d, %@
The critical rule: a placeholder must survive translation unchanged. If "Hello, {userName}!" becomes "Hola, !" in Spanish, the runtime substitution fails silently or throws an error. The placeholder isn't content to be translated, it's a contract between your code and your string.
ICU MessageFormat: Why It Matters
The ICU MessageFormat specification is the industry standard for encoding plurals, gender selection, and nested substitutions into translatable strings. A typical ICU message looks like this:
{count, plural, =0 {No messages} one {# message} other {# messages}}
This string contains syntax that most translation engines will attempt to translate literally, turning plural into pluriel, or other into otro, which breaks the parser entirely. ICU MessageFormat strings require special handling because:
- Keywords are reserved. plural, select, one, other, few, many are ICU keywords, not translatable content.
- Nesting creates fragile structures. A misplaced brace or a translated keyword collapses the entire message.
- Translators need the translatable segments only. The phrases No messages, # message, and # messages are the content; everything else is structure.
The safest approach is to decompose ICU messages into their translatable leaf segments before sending them to a translation API, then reassemble them afterward. Sending the raw ICU string through a generic translation endpoint is the single most common source of localization-breaking bugs.
HTML, Markdown, and XML Tag Handling
Rich-text content introduces inline tags that must be preserved through translation:
<p>Welcome to <strong>{appName}</strong>. Click <a href="{url}">here</a>.</p>
A translation API that doesn't understand markup might:
- Translate attribute values (href contents)
- Reorder opening and closing tags incorrectly
- Strip tags entirely
- Merge or split tags across sentence boundaries
Each of these produces broken output. The challenge is that tags are interleaved with translatable text, they aren't purely structural and can't simply be removed and re-inserted. Tag positions often shift legitimately during translation because word order changes between languages.
How Major Translation APIs Handle Formats
Ollang treats format preservation as a core part of the execution layer: placeholder detection, ICU decomposition, markup-aware segmentation, and automated validation run as integrated steps around any translation call so teams avoid building and maintaining separate wrapping and validation pipelines.
Google Cloud Translation: format and glossaryConfig
Google's Cloud Translation API (v3) provides two key parameters for format control:
- mimeType: Set to text/html to tell the API that your source contains HTML markup. The engine will attempt to preserve tag structure and avoid translating attribute values. For plain text with placeholders, use text/plain, but note that the API has no built-in awareness of placeholder syntax like {variable}.
- glossaryConfig: Attach a glossary resource to force specific terms through untranslated. This can be used to lock placeholders: add entries like {userName} → {userName} to your glossary, and the API will attempt to pass them through.
A representative request using mimeType and a glossary:
{
"sourceLanguageCode": "en",
"targetLanguageCode": "de",
"contents": ["<p>Hello, <b>{userName}</b>!</p>"],
"mimeType": "text/html",
"glossaryConfig": {
"glossary": "projects/my-project/locations/us-central1/glossaries/placeholders"
}
}
The glossary approach works but requires maintaining a glossary resource that enumerates every placeholder pattern you use. For projects with many dynamic tokens, this becomes a maintenance burden.
DeepL API: tag_handling and preserve_formatting
DeepL offers more granular control through its API documentation:
- tag_handling: Set to xml or html to activate tag-aware translation. When enabled, DeepL parses the markup and preserves tag structure.
- ignore_tags: Specify tag names whose content should not be translated (e.g., code, var).
- non_splitting_tags: Tags that should not cause sentence segmentation (e.g., strong, em).
- preserve_formatting: When enabled, the API attempts to maintain whitespace, line breaks, and other formatting cues.
A curl call demonstrating DeepL's tag handling:
curl -X POST 'https://api-free.deepl.com/v2/translate' \
-d auth_key=YOUR_KEY \
-d "text=<p>You have <var>{count}</var> new messages.</p>" \
-d "target_lang=FR" \
-d "tag_handling=html" \
-d "ignore_tags=var"
By wrapping {count} in a <var> tag and adding var to ignore_tags, you instruct DeepL to leave the placeholder untouched. This wrapping technique is one of the most reliable cross-platform strategies for protecting arbitrary placeholders.
Amazon Translate: HTML and Custom Terminology
Amazon Translate automatically detects and preserves HTML tags when the ContentType is set to text/html. For placeholder protection, Amazon offers Custom Terminology, a CSV or TMX file uploaded to the service that forces specific source terms to map to specific target terms (or to themselves).
For ICU and non-HTML placeholders, the same wrapping strategy applies: enclose placeholders in tags like <span translate="no"> before sending, and strip the wrapper tags from the response.
Comparing API Format-Handling Capabilities
| Capability | Ollang | Google Cloud Translation | DeepL | Amazon Translate |
|---|---|---|---|---|
| HTML tag preservation | Yes, with automatic detection | Via mimeType: text/html | Via tag_handling=html | Via ContentType: text/html |
| XML tag handling | Yes | Limited | Via tag_handling=xml | Limited |
| Placeholder locking | Built-in placeholder detection and locking | Via glossary entries | Via ignore_tags wrapping | Via Custom Terminology |
| ICU MessageFormat awareness | Segment-level decomposition | No native support | No native support | No native support |
| Glossary / terminology control | Integrated glossary and TM | Glossary API resource | Glossary API | Custom Terminology upload |
| Markdown preservation | Yes | No native support | Limited | No native support |
| Translation quality review | Built-in QA checks including placeholder validation | Manual post-processing | Manual post-processing | Manual post-processing |
Ollang's approach differs from point-API solutions in that placeholder detection, format preservation, and quality validation are part of the execution layer rather than requiring manual pre- and post-processing. Where other providers require you to build wrapping, glossary management, and validation pipelines yourself, Ollang surfaces format-aware controls and validations as part of the workflow, reducing the engineering surface area for teams that localize across text, software UI, documents, and multimedia.
Escaping Strategies and Placeholder Locking
Wrapping Placeholders in Non-Translatable Tags
The most portable strategy across translation APIs is to wrap placeholders in tags that signal "do not translate." Before sending content to any API:
- Detect placeholders using regex patterns appropriate to your framework (e.g., \{[a-zA-Z_]\w*\} for named placeholders, %[ds] for printf-style).
- Wrap each match in a protective tag:
import re
def protect_placeholders(text):
return re.sub(
r'(\{[a-zA-Z_]\w*\})',
r'<span translate="no">\1</span>',
text
)
# Input: "Hello, {userName}! You have {count} items."
# Output: "Hello, <span translate=\"no\">{userName}</span>! You have <span translate=\"no\">{count}</span> items."
- Send with HTML tag handling enabled so the API respects the translate="no" attribute or the tag boundary.
- Strip wrappers from the translated response.
This approach works with Google (mimeType: text/html), DeepL (tag_handling=html), and Amazon (ContentType: text/html). It's the closest thing to a universal placeholder-locking mechanism.
Tokenization and Replacement
An alternative approach replaces placeholders with unique tokens before translation, then restores them afterward:
function tokenize(text) {
const map = {};
let counter = 0;
const tokenized = text.replace(/\{[a-zA-Z_]\w*\}/g, (match) => {
const token = `__PH${counter}__`;
map[token] = match;
counter++;
return token;
});
return { tokenized, map };
}
function detokenize(text, map) {
return Object.entries(map).reduce(
(result, [token, original]) => result.replace(token, original),
text
);
}
This works when the token pattern (__PH0__) is unlikely to be altered by the translation engine. The risk is that some engines may transliterate or drop tokens that look like gibberish. Testing with your specific API and language pairs is essential.
Handling ICU Messages Safely
For ICU MessageFormat strings, neither wrapping nor tokenization is sufficient on its own. The recommended pipeline:
- Parse the ICU message into an AST using a library like intl-messageformat-parser or messageformat.
- Extract only the translatable leaf strings (the text inside {...} branches).
- Send each leaf string individually to the translation API.
- Reassemble the translated leaves back into the ICU structure.
# Conceptual pipeline for ICU handling
icu_source = "{count, plural, =0 {No items} one {# item} other {# items}}"
# Step 1-2: Parse and extract leaves
leaves = ["No items", "# item", "# items"]
# Step 3: Translate each leaf independently
translated_leaves = [translate(leaf, target="es") for leaf in leaves]
# Result: ["Sin elementos", "# elemento", "# elementos"]
# Step 4: Reassemble
icu_translated = "{count, plural, =0 {Sin elementos} one {# elemento} other {# elementos}}"
This keeps the ICU skeleton intact and only translates the human-readable segments.
Ready to see Ollang in action?
Talk to our team about your localization goals and see how the Ollang platform fits your workflow.
Segmentation and Sentence Splitting
Why Segmentation Can Break Markup
Translation APIs typically segment input into sentences before translating. If a sentence boundary falls in the middle of a tag pair, the result can be catastrophic:
<!-- Source -->
<p>Welcome to our platform. <strong>Get started today</strong> by signing up.</p>
<!-- After bad segmentation -->
Segment 1: "<p>Welcome to our platform."
Segment 2: "<strong>Get started today</strong> by signing up.</p>"
Segment 1 now has an unclosed <p> tag. Segment 2 has a closing </p> without a matching open. When the segments are translated independently and reassembled, the HTML is broken.
Strategies for Safe Segmentation
- Send complete structural blocks. Don't split within a paragraph, send the entire <p> element as one unit.
- Use non_splitting_tags (DeepL) or equivalent configurations to prevent the API from splitting on inline tags like <strong>, <em>, <a>.
- Pre-segment at safe boundaries. Use sentence boundary detection (e.g., Unicode UAX #29) on the plain-text content, then map segments back to their enclosing markup.
- Preserve block-level structure. Translate block elements (<p>, <li>, <h1>) individually, but keep inline elements within their parent block.
For Markdown content, convert to an AST (using a parser like remark or markdown-it), extract translatable text nodes, translate them, and reassemble. Never send raw Markdown to a translation API that doesn't explicitly support it, the engine will likely mangle link syntax, code fences, and heading markers.
Validation: Catching Breakage Before Production
Lint Rules for Placeholder Integrity
Add static analysis to your localization pipeline that checks every translated string against its source:
- Placeholder count match: The translated string must contain the same set of placeholders as the source.
- Placeholder identity: Each placeholder token must appear exactly as in the source, no partial translations, no case changes.
- Balanced tags: Every opening tag must have a matching closing tag.
- ICU keyword preservation: Keywords like plural, select, one, other must remain in English.
A simple lint function:
import re
def validate_placeholders(source, translated):
pattern = r'\{[a-zA-Z_]\w*\}'
source_phs = sorted(re.findall(pattern, source))
translated_phs = sorted(re.findall(pattern, translated))
if source_phs != translated_phs:
return False, f"Mismatch: expected {source_phs}, got {translated_phs}"
return True, "OK"
Round-Trip Tests
A round-trip test translates a string from the source language to the target and back, then compares the placeholder structure (not the text content) of the original and the round-tripped version. If placeholders are preserved in both directions, the API's handling is likely safe for that string pattern.
Run round-trip tests for every placeholder pattern in your codebase, across all target languages. Some languages expose API weaknesses that others don't, right-to-left scripts, CJK languages, and languages with complex plural rules are common sources of edge cases.
Snapshot Diffs
Treat your translated string files like code. Store them in version control and use snapshot diffing to catch unexpected changes:
# In CI, after running translation
diff translated/es.json translated/es.json.snapshot
Any diff that shows a placeholder appearing, disappearing, or changing should fail the build. This catches regressions introduced by API behavior changes, glossary updates, or new source strings that weren't properly protected.
Pseudolocalization as a Smoke Test
Pseudolocalization replaces translatable characters with accented equivalents while preserving placeholders and markup: "Hello, {userName}!" becomes "[Ĥéļļö, {userName}!]". This technique validates three things simultaneously:
1. Placeholders survive the string-processing pipeline.
2. The UI can handle longer strings (pseudo strings are typically 30-50% longer).
3. Character encoding is correct throughout the stack.
Run pseudolocalization as the first step in your CI localization pipeline, before spending API credits on real translations. If you want to see how built-in quality review and pseudolocalization fit into a production workflow, see how Ollang integrates quality review directly into the translation workflow.
Common Failure Modes and How to Catch Them in CI
Dropped Placeholders
What happens: The translated string omits one or more placeholders. At runtime, the substitution either fails silently (showing raw template syntax) or throws an error.
Example:
Source (en): "Welcome back, {firstName}! You have {count} notifications."
Translated (ja): "おかえりなさい!通知があります。"
Both {firstName} and {count} are gone. The lint rule described above catches this immediately.
CI integration:
# GitHub Actions example
- name: Validate placeholders
run: python scripts/validate_translations.py --source en.json --target ja.json
# Exits non-zero if any string has mismatched placeholders
Broken ICU Syntax
What happens: The translation engine translates ICU keywords or misplaces braces, producing an unparseable message.
Example:
Source: "{count, plural, one {# item} other {# items}}"
Broken: "{count, pluriel, un {# article} autre {# articles}}"
The parser encounters pluriel and un instead of plural and one, and throws a syntax error.
Detection: Parse every ICU string in the translated output with your ICU library. If parsing throws, the string is broken. Add this as a CI step:
from icu import MessageFormat
def validate_icu(translated_string, locale):
try:
MessageFormat(translated_string, locale)
return True
except Exception as e:
return False
Mangled HTML Tags
What happens: Tags are reordered, nested incorrectly, or partially translated.
Example:
Source: "Click <a href=\"/settings\">here</a> to update."
Broken: "Cliquez <a href=\"/paramètres\">ici pour mettre à jour.</a>"
The href value was translated (breaking the link), and the </a> tag moved to the end of the sentence (changing what's clickable).
Detection: Parse the HTML output and compare the tag structure to the source. Tools like cheerio (Node) or BeautifulSoup (Python) can extract tag trees for structural comparison.
Encoding and Escaping Corruption
What happens: Special characters like &, <, > are double-escaped or unescaped in the response, producing display artifacts or XSS vulnerabilities.
Example:
Source: "Price: {amount} & tax included"
Broken: "Prix : {amount} & taxes incluses"
Detection: Check for known double-encoding patterns (&, <) and for unescaped HTML entities in contexts where they should be escaped.
Building a CI Pipeline for Format Integrity
A complete CI pipeline for translation format integrity includes these stages:
- Extract source strings from code (using tools like formatjs extract, i18next-parser, or custom extraction scripts).
- Pre-process strings: wrap placeholders, decompose ICU messages, normalize markup.
- Pseudolocalize as a smoke test, validate that all placeholders and tags survive the pre/post-processing pipeline.
- Translate via API, with appropriate format parameters (mimeType, tag_handling, glossaries).
- Post-process translated strings: strip wrapper tags, reassemble ICU messages, restore tokenized placeholders.
- Validate with lint rules: placeholder count and identity, ICU parseability, HTML structure, encoding integrity.
- Snapshot diff against the previous translation baseline.
- Fail the build on any validation error.
This pipeline turns format preservation from a hope into a guarantee. Every translated string is verified before it reaches production.
Prefer not to assemble and maintain this pipeline yourself across apps, docs, and multimedia? See a format-safe pipeline demo.
FAQ
Can I send ICU MessageFormat strings directly to a translation API?
Sending raw ICU strings to most translation APIs is risky. Most engines have no native understanding of ICU syntax and will often translate reserved keywords (plural, one, other) or misplace braces. The safe approach is to parse the ICU message, extract only the translatable text segments, translate those individually, and reassemble the ICU structure from the translated pieces.
How do I protect placeholders if my translation API doesn't support glossaries?
Use the tag-wrapping technique: wrap each placeholder in a <span translate="no"> tag (or equivalent non-translatable tag), send the content with HTML tag handling enabled, and strip the wrapper tags from the response. Alternatively, replace placeholders with unique tokens before translation and restore them afterward, though this carries a small risk of token corruption with some engines.
What's the best way to test placeholder preservation in CI?
Combine three techniques: static lint rules that compare placeholder sets between source and translated strings, pseudolocalization that validates the entire pre/post-processing pipeline without consuming API credits, and snapshot diffs that catch regressions between translation runs. Parse ICU strings with your ICU library and validate HTML structure with a DOM parser. Fail the build on any mismatch.
How does Ollang handle format and placeholder preservation differently from standalone APIs?
Ollang operates as an execution layer rather than a raw translation endpoint. Placeholder detection, format-aware segmentation, ICU decomposition, and post-translation quality checks, including placeholder validation and tag integrity verification, are built into the workflow rather than requiring separate engineering. This reduces the amount of custom infrastructure your team must build and maintain.
Enforce Format Integrity Across Every Language
Placeholder and format preservation isn't a nice-to-have, it's the difference between a localized product that works and one that breaks in production. The techniques in this guide, tag wrapping, ICU decomposition, lint rules, pseudolocalization, and CI-integrated validation, give you the tools to enforce integrity at scale. But building and maintaining these pipelines across dozens of languages and content types is substantial engineering work.
Ollang handles this as part of its localization execution layer, covering text, software, documents, and multimedia with built-in quality controls. If you're ready to stop building placeholder-protection infrastructure and start shipping localized products with confidence, Ollang can help.
Ready to see Ollang in action?
Talk to our team about your localization goals and see how the Ollang platform fits your workflow.
Ship format-safe translations
Published on August 13, 2026