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.
