A placeholder is a hole in a string that the application fills at runtime. It looks like text, sits in the middle of a sentence, and is the single most common cause of a localized build that crashes or prints nonsense. Translators are asked to work around dozens of incompatible syntaxes, usually with no documentation about what any of them will contain.
This is the map.
Who emits what
| Syntax | Where it comes from |
|---|---|
{0} {1} | .NET composite formatting, Java MessageFormat, ICU MessageFormat |
{name} | Python str.format, Rust format!, most YAML and JSON i18n formats |
%s %d %f | C printf and everything descended from it — PHP, Go, Android string resources |
%1$s %2$d | Positional printf; mandatory in Android resources once a string has two arguments |
%(name)s | Python printf-style dict interpolation — still everywhere in gettext-based Django and Flask code |
{{x}} | Handlebars and Mustache, Vue, Angular interpolation, i18next by default |
$t(key) | i18next nesting — pulls in the value of another translation key |
#x# | ColdFusion-style hash variables, and some older CMS and TMS templating |
<0> <1> | react-i18next Trans components; the digit indexes a React child, not a variable |
Several more show up often enough to recognise on sight: ${name} in JavaScript
template literals, %{name} in Ruby I18n and Elixir gettext, :name in Rails,
%@ in Objective-C and Swift format strings. The first two are caught by this
editor’s brace pattern through their inner core; the last two are not currently
detected, and knowing that is more useful than assuming full coverage.
The two that surprise people are $t() and <0>.
$t(key) does not interpolate a value — it inlines another string from the same
resource bundle. Translate the key inside it and the lookup fails, so the
application renders the raw text $t(common.cancel) to a user.
<0> is not markup and not a variable. In react-i18next, a source string like:
{ "terms": "Read the <0>terms of service</0> before continuing." }
means “wrap the words terms of service in whatever the first child element of this component is” — usually a link. The digits are array indices into the JSX children. Renumber them and the link wraps the wrong words; delete the closing half and the link swallows the rest of the sentence.
Why they are untouchable
Placeholders are not text with special punctuation. They are instructions to a
formatting function, and the function has exactly three behaviours when a token
is wrong: it throws, it prints the literal token to the user, or — the dangerous
one — it silently reads the wrong argument. .NET raises a FormatException on a
malformed index; Python raises KeyError on an unknown name; printf with a
missing argument reads whatever is next on the stack.
Two rules follow, and they pull in opposite directions.
The set of tokens must match. Every placeholder in the source must appear in
the target, exactly as written, the same number of times. Case matters —
{userName} and {UserName} are different keys in every case-sensitive lookup.
The order may legitimately change. Rearranging interpolations is the entire point of positional forms, and a target language with different word order will need them moved. So a QA check has to compare multisets, never sequences. This editor’s placeholder check does exactly that: it reports missing and extra tokens independently, and never complains about order.
Reporting both directions is deliberate. The failure that actually ships broken
builds — {name} retyped as {nome} — is never one event. It is a missing token
plus an invented one, and showing only the missing half hides the typo that
caused it.
Two more details from the implementation, because they explain findings you will see:
- Whitespace inside a token is normalized away.
{{ count }}and{{count}}interpolate the same variable, so a translator who tightened the spacing has not broken anything and should not be flagged. - Placeholders are masked out before the number check runs. The digits in
%1$s,{0}and<0>are argument indices, not numbers a translator is meant to reproduce, and without masking every positional string would produce a phantom “number missing from target” finding.
The detection patterns are also deliberately conservative in two spots, because
a false error at severity error costs more than a missed one. The printf
pattern omits the space flag, so “50% of users” is not read as a token. And the
hash pattern requires a word character immediately after the opening #, so
“C# and F#” and Markdown headings stay clean.
How they actually get corrupted
These are the failure patterns worth checking for by name.
Full-width substitution. A CJK input method converts % to % (U+FF05) or
{ to {. The characters are visually near-identical at 15px and functionally
dead. This is the single most common placeholder corruption in Japanese and
Chinese targets, and it is invisible in a proofreading pass.
Bidi retyping. In an RTL target, %1$s displays with its parts reordered.
Translators who retype the token in the order they see it produce s$1% in the
file. The rule for RTL work is absolute: copy tokens from source, never retype
them.
Autocorrect on the token name. A word processor capitalizes the first letter
after a period, turning {userName} into {UserName}. The build succeeds. The
string renders the literal braces to the user.
Machine translation translating the inside. MT engines that were not given
placeholder protection will happily render {{count}} as {{compte}} or
$t(common.cancel) as $t(commun.annuler). The key stops resolving and the
framework falls back to printing the key itself.
Dropping the positional index. A source with two %s tokens needs them
swapped in the target. A translator swaps the words but writes plain %s twice —
the arguments are now the wrong way round, and nothing errors. Android’s lint
catches mixed positional and non-positional forms; most pipelines catch nothing.
Breaking <0> pairs. Someone “fixes” <0> into <0/>, or drops </0>
because it looked like a stray tag. The wrapped element loses its children and
the link text disappears from the rendered page.
French spacing rules applied to a percent conversion. Typographic French puts
a space before % as a percent sign. Applied to a printf string like %d%%,
the same habit produces % d%% and the conversion dies.
What to do about it
For translators: never retype a token, always copy it; run the placeholder check
before delivery, not after; and when a source string gives no clue what {0}
contains, ask — a placeholder that turns out to be a number changes the grammar
of the sentence around it in most languages.
For the developers upstream: named arguments beat positional ones, ICU
MessageFormat beats bare printf for anything with a plural or a gender, and a
one-line developer comment next to each string (“{0} is a file name”) removes
more translation defects per minute of effort than any other intervention
available to you.
This editor ships the placeholder check on by default, at error severity, for every format it opens. Click a finding to jump to the segment; the token is shown exactly as written in each side, which is usually enough to see the corruption without hunting for it.