Site icon Jesús Amieiro

Placeholders in software translation: a guide for translators

If you translate software, sooner or later you run into strings like this one from the Galician translation of WordPress core:

Enlarged image %1$s of %2$s: %3$s

Or this one from Firefox on Mozilla Pontoon:

Close { $tabCount } tabs

Those %1$s and { $tabCount } fragments are placeholders: marks that the program replaces with real content when it runs. They are a common source of doubt for volunteer translators, and mistakes in them can break what users see. This guide covers the main placeholder families, plus other strange symbols (&_@#) that aren’t always placeholders but can cause just as much confusion. The examples come from free software projects with active Galician translation teams, mainly WordPress and Firefox, but the basic principles are useful when translating into any language.

What a placeholder is

A translatable string is often not a finished sentence. It is a template. When WordPress shows you “Enlarged image 3 of 10: Sunset at the beach”, the developer did not write that sentence anywhere. They wrote the template above, and the program filled in the gaps at runtime: %1$s became “3”, %2$s became “10”, and %3$s became the image title.

The Galician translator kept the template structure intact:

Enlarged image %1$s of %2$s: %3$s
Imaxe ampliada %1$s de %2$s: %3$s

This leads to two rules that cover most everyday cases:

  1. Copy each placeholder into your translation exactly as it appears in the original. Character by character. %1$s is not %1s, not % 1$s, not %1$S{ $tabCount } is not { $tabcount } and not { contadorDeLapelas }. Changing or omitting a placeholder can cause a validation warning, broken output, a runtime error or, in some projects, a failed build.
  2. Move numbered or named placeholders when the format allows it. If your language needs the sentence in a different order, placeholders such as %1$s and { $tabCount } can normally move with the content they represent. Unnumbered placeholders such as %s … %s are different: changing their order also changes which value appears in each position.

Everything else in this article is detail on top of those two rules.

The printf family: %s, %d, %1$s

One of the oldest and most widespread notations comes from the C programming language and its printf function. Variants of it appear in PHP, Python, and many other environments. WordPress, being a PHP project, uses it extensively. The basic marks are:

MarkReplaced with
%sA text string (a name, a title, a URL…)
%dA whole number
%fA decimal number
%%A literal percent sign (this one is not a variable: “50%%” displays as “50%”)

When a string has more than one placeholder, good developers number them: %1$s is the first argument, %2$s the second, and so on. A real example from WordPress core in Galician:

Ability "%1$s" callback threw an exception: %2$s
A función de retorno da capacidade «%1$s» lanzou unha excepción: %2$s

Notice two things. The placeholders were copied exactly from the original. And the translator still adapted everything around  them, including replacing the English quotation marks with Galician «» guillemets.

The numbers matter because they free you from English word order. Imagine this string:

%1$s replied to %2$s

If your translation works better as “the post received a reply”, you can write:

%2$s recibiu unha resposta de %1$s

The program doesn’t care about the order in which the placeholders appear in your sentence. It cares about the numbers: argument 1 goes wherever %1$s is, argument 2 goes wherever %2$s is. This is why an unnumbered pair like %s … %s is a problem: the first value always lands in the first %s, so you cannot swap them. If a string with two or more unnumbered placeholders forces an unnatural order in your language, report it to the developers; in WordPress this is considered a bug in the original string.

Named placeholders: {name}, %(name)s, %{name}, :name

Many environments use names instead of position numbers. The resulting notations may look similar, but they belong to different formatting systems and do not necessarily follow the same rules:

NotationWhere you’ll see itExample
{name}Python str.format and several library-specific formatsHello, {username}!
%(name)sPython (older style, common in Django)%(count)d files deleted
%{name}Ruby on RailsSigned in as %{user}
:nameLaravel (PHP)The :attribute field is required.
{{name}}Handlebars/Mustache and similar template systems{{count}} items in your cart
###NAME###WordPress emails and a few other projects, as an ad-hoc conventionHowdy ###USERNAME###

The usual rule is the same as before, with one extra warning: the name is an identifier, not a word to translate. {username} stays {username} even though “username” is an ordinary English word. Translating the name inside the braces is a common mistake. Because similar-looking syntax can behave differently, check the project’s instructions before changing punctuation, spacing or formatting around it.

Fluent: how Firefox does it

Mozilla developed a localization system called Fluent, now used for most Firefox interface strings translated through Pontoon. Fluent calls an expression inside braces a placeable. A variable is named with a dollar sign and can appear inside one. Here is a pair from the Galician translation:

Close { $tabCount } tabs
Pechar { $tabCount } lapelas

Fluent also supports references. A message reference, such as { menu-settings }, reuses another message and has no dollar sign. A term reference starts with a dash and points to reusable vocabulary such as a product name:

Welcome to { -brand-short-name }
Benvida ao { -brand-short-name }

{ -brand-short-name } expands to a value such as “Firefox” or “Firefox Nightly”, depending on the build. Keeping the term instead of hardcoding “Firefox” lets the same string work with different editions of the browser.

One Galician string on Pontoon offers a useful detail: an original such as ${ $amount } (a dollar sign followed by an amount) can become { $amount } €. The placeholder { $amount } stays untouched while the currency symbol and its position are localized. That is the right instinct when the project context confirms the currency: adapt the text, preserve the mark.

Fluent also lets a translation branch depending on a variable, which is how it handles plurals:

tab-close-warning =
    { $tabCount ->
        [one] Close { $tabCount } tab?
       *[other] Close { $tabCount } tabs?
    }

As a translator you can adapt the branches to match your language’s plural categories, subject to the requirements of the particular message and project. Galician, like English, uses the cardinal categories one and other; some languages use as many as six. The Fluent syntax guide is short and written with translators in mind, making it a useful starting point if you contribute to Mozilla projects.

ICU MessageFormat: plurals with a # inside

ICU MessageFormat is used by many localization libraries and applications, including some Java, JavaScript and Flutter workflows. Android and Apple platforms also have their own native resource and plural formats, so do not assume that every mobile app uses ICU syntax. ICU combines named placeholders with plural and selection logic in the same string:

{count, plural,
    one {You have # unread message}
    other {You have # unread messages}
}

Here is where the mysterious # shows up: inside an ICU plural block, # normally represents the selected number. The example above displays as “You have 1 unread message” or “You have 12 unread messages”. In advanced patterns that declare an offset, it represents the number minus that offset. Keep the #, translate the words around it, and use the plural categories required by your language. The keywords onefewmany and other are fixed identifiers from the CLDR plural rules; don’t translate them either.

Outside an ICU plural block, # is usually just a hash character with no special meaning, or a comment marker in configuration-style files. The same character, different meanings depending on where it appears.

&, _, @, #: symbols that depend on context

Some characters look like placeholders but serve other purposes, and the same character can mean different things in different projects. These are among the most common:

& and _ : keyboard accelerators

In desktop applications built with Windows toolkits or Qt, an ampersand inside a menu label marks the keyboard shortcut for that entry: &File means the user can press Alt+F to open the File menu, and the F appears underlined. GTK applications (GNOME, and many apps you’ll find in Linux distributions) use an underscore for the same job: _File.

Accelerators do concern the translator, because you place them yourself:

&, … : HTML entities

The same ampersand, followed by a name and a semicolon, is an HTML entity: &amp; is a literal &, &hellip; is an ellipsis (…), &nbsp; is a non-breaking space. Copy them as they are. And when a string contains HTML tags, such as <strong>Warning</strong>, translate the text between the tags and leave the tags themselves intact and balanced: every opening tag keeps its closing partner.

@ : usually iOS, sometimes just an at sign

If you translate Apple software or iOS apps, you will meet %@: it is the printf-family placeholder for “an object”, in practice a string, in Apple’s Objective-C world. %1$@ and %2$@ are its numbered versions, and everything said above about printf applies. Outside Apple platforms, an @ is normally just an at sign in an email address or a social media handle, with no special behavior.

\n, \t, \” : escape sequences

In many resource formats, a backslash followed by a character forms an escape sequence: \n is a line break, \t a tab, and \" can represent a quotation mark inside a quoted string. Preserve the intended line break, tab or quotation mark, but place it where the translated text requires. Do not add, remove or alter escapes unless you understand the resource format, because their exact rules vary.

Do I need to learn the programming language?

Short answer: no. This question comes up in almost every localization workshop, and the reasoning behind it makes sense: the strings look like code, so it seems you’d need to study the code to understand them. You don’t.

The notation usually comes from the localization framework, formatting API or resource format chosen by the project. The programming language can influence that choice, but it rarely means that a translator needs to study the language itself. Firefox is built with several programming languages, yet translators now encounter Fluent in most of its interface strings, alongside some legacy or platform-specific formats. WordPress is PHP, but translators mainly need to recognize gettext strings and printf placeholders—knowledge that also transfers to other gettext-based projects. Start by identifying the localization system and reading the project’s translator documentation rather than reaching for a general programming book.

This table maps the ecosystems you’re most likely to meet as a free software translator:

Project / ecosystemSystemPlaceholders look likeWatch out for
WordPress and many gettext-based PHP projectsgettext + printf%s%d%1$sHTML tags in strings; %% for a literal %
Current Firefox and Thunderbird interface stringsMainly Fluent, with some legacy or platform-specific resources{ $var }{ message }{ -term }Placeables, references, attributes and select expressions
GNOME, GIMP, Inkscape, most of classic Linuxgettext + printf%s%d_ accelerators in menus
KDE and Qt applicationsQt Linguist%1%2& accelerators; no letter after the number
Android apps using native resourcesAndroid string resources%1$s%1$dXML escaping, formatted strings and dedicated plural resources
iOS and macOS apps using native resourcesApple string resources and String Catalogs%@%1$@%dThe plural format depends on the resource type and toolchain
Web apps (React, Vue, Angular…)Depends on the chosen i18n library; ICU is one common option{name}{count, plural, …}, or library-specific syntaxSimilar-looking placeholders may follow different rules
Ruby on Rails appsRails i18n%{name}YAML files: indentation matters

When you join a new project, first look for its translator documentation, style guide and comments attached to the string. Established projects often explain which systems they use and what their variables look like. Those resources will usually answer your localization questions more directly than a general programming book.

Plurals deserve their own warning

“1 comment” but “2 comments”. Plural handling is where placeholders and grammar meet, and each system does it differently. In gettext (WordPress, GNOME), the developer provides two English forms and your translation tool shows you as many boxes as your language needs:

%s comment  /  %s comments
%s comentario  /  %s comentarios

In common gettext configurations, Galician and English use two plural forms, Czech uses three and Arabic uses six. Gettext reads the appropriate formula from the translation file header, so the translation tool can show the required number of fields. Fluent and ICU instead put the alternatives inside the message, as you saw above, and allow the localization to match its own plural categories. In every system the same principle holds: never infer the target language’s plural rule from the English wording alone; use the plural mechanism provided by the project.

Let the tools check your work

You don’t have to catch placeholder mistakes by eye. The main platforms validate them for you:

Where to learn more

A short list of references that explain this in more depth, all free:

Exit mobile version