Localizely logo
  • Home
  • Getting started
  • How it works
  • Pricing
  • FAQ
Sign up

Universal placeholders

One project for Android, iOS, web, and every other platform.

Documentation

Universal placeholders let you translate an app once and ship it to Android, iOS, web, and every other platform from a single Localizely project. Placeholders are stored in one form, {name}, and converted to the syntax of each file format when you upload and download files.

The problem

Every platform writes placeholders differently. Android says Hello %1$s, iOS says Hello %1$@, Flutter says Hello {name}, and a web app might say Hello {{name}}. Uploading files from two platforms into one project therefore either overwrites one syntax with the other on every upload, or forces you to keep two copies of the same string key and translate everything twice.

Universal placeholders remove the difference. Translators see and type {name} regardless of the platform, and every platform gets its own syntax back in the files it downloads.

How it works

  • One stored form. Translations are stored with placeholders written as {name}, the syntax of ICU messages.
  • Conversion at the file boundary. An upload converts the syntax of the file into the stored form. A download converts the stored form into the syntax of the requested file format.
  • Placeholder definitions. Each string key remembers its placeholders: the name, the argument position, the value type, and the exact token each platform wrote for it. This is how iOS gets its %lld back while Android gets %d.

Here is one stored translation and what each download produces from it:

File formatTranslation
Stored in LocalizelyHello {name}, you have {count} new messages
Android ResourcesHello %1$s, you have %2$d new messages
Apple Strings, Stringsdict, and String CatalogHello %1$@, you have %2$lld new messages
Flutter ARBHello {name}, you have {count} new messages
Key-Value JSON with i18next syntaxHello {{name}}, you have {{count}} new messages
.NET ResourcesHello {0}, you have {1} new messages
Ruby on Rails YAMLHello %{name}, you have %{count} new messages
Gettext POHello %1$s, you have %2$d new messages

Supported file formats

File formats with a fixed placeholder syntax are converted automatically, without any setup:

File formatPlaceholder syntax
Android Resources (.xml)Java printf: %s, %1$s, %d, %.2f
Apple Strings (.strings), Stringsdict (.stringsdict), and String Catalog (.xcstrings)Apple printf: %@, %1$@, %lld, %.2f
Flutter ARB (.arb)ICU: {name}, no conversion needed
.NET Resources (.resx){0}, {1:F2}
Ruby on Rails YAML (.yml)%{name}, %<count>d
Gettext PO (.po, .pot)C printf: %s, %1$s, %(name)s

Generic file formats have no placeholder syntax of their own: Key-Value JSON, Java Properties, CSV, Excel, and XLIFF. For these, the syntax is chosen by you. Set the default under Settings, tab Placeholders, as the Placeholder syntax of generic file formats, and override it for a single file with the Placeholder syntax in the file option of the upload and download, or with the placeholder_format parameter of the API, the CLI, and the configuration file. The available syntaxes are Android / Java printf, Apple printf, C / gettext printf, ICU, .NET, Ruby, and i18next. Choose None to leave the text as it is.

Switching a project to universal placeholders

Universal placeholders are enabled per project. New projects can switch before the first upload, and existing projects can switch at any time:

  1. Open Settings, tab Placeholders, and click Switch to universal placeholders. The dialog detects the syntax the translations are written in from the project itself and lets you change it.
  2. Review the preview. It shows how many string keys will be rewritten and in which syntax they were detected, how many are already universal or have no placeholders, which keys are flagged, and the translation texts that change, each with its text before and after, so nothing is applied unseen.
  3. Confirm the switch. A background job rewrites every translation in every branch of the project. Review states and the translation history are kept, and the rewrite does not count as a translation change.

A few things to know about the switch:

  • Flagged string keys mix placeholder syntaxes, for example %@ and {{name}} in one translation, or different syntaxes in different languages. They are left untouched, so check them in the editor after the switch.
  • The chosen syntax decides how placeholders valid in more than one printf flavor, such as %d, are read, and that single braces in Android, iOS, Ruby, and i18next text are literal text rather than placeholders.
  • Uploads are refused while the job runs. Downloads keep working.
  • If the job is interrupted, for example by a deployment, nothing is lost. Every change it makes is a new version of a translation, and the switch continues on its own from where it stopped, usually within a minute. The settings tab shows where it stands and lets you continue it yourself as well.
  • You can switch back at any time. Translations that have not changed since the switch get their previous text back exactly, tokens, positions, and quoting included. Anything edited or uploaded while universal is rendered in the syntax you choose, keys the switch skipped are left as they are, and the placeholder definitions are kept for a later switch.

Uploading files

  • Upload files from any platform into the same project. A translation that already exists with the same text is left unchanged even if the file spells its placeholders differently, so an upload from the other platform creates no new versions and changes no review states.
  • The main language defines the placeholders of a string key. Uploads of other languages are matched against them, by name for formats with named placeholders and by position for the others.
  • Placeholders from positional formats such as Android and iOS get automatic names, arg1, arg2, and so on. The first main-language upload in a format with named placeholders, for example a Flutter ARB or an i18next JSON file, gives them their real names in every language. You can also rename a placeholder in the editor.
  • Placeholder names are identifiers: letters, digits, and underscores, not starting with a digit. A file with a name that does not fit, for example {{user.name}}, keeps its token remembered, while the placeholder keeps its automatic name.
  • With the Overwrite option enabled, an upload may also change the token a platform remembered for a placeholder, for example from %d to %lld.

Downloading files

  • Each platform gets the token it uploaded. When a platform never uploaded a string key, its token is derived from the type of the placeholder, for example %@ for text and %lld for a whole number on iOS.
  • A translation that uses its placeholders in the order of the source text comes back with the tokens it was uploaded with. Printf formats write explicit positions, such as %2$d, when a translation reorders the placeholders or when a placeholder has no token of that platform yet, so that translators can reorder placeholders freely.
  • A literal percent sign is written as %% in printf formats when the translation has a placeholder. A literal brace is quoted the ICU way in the stored text, '{' and '}', and written as a plain brace on download.
  • Tokens that no other platform can express, such as %n or %tY on Android, pass through unchanged.

Working in the editor

The translation editor has a Placeholders tab for every string key. It lists the placeholders of the key with their type and the token each platform wrote, inserts a placeholder into the translation with one click, renames a placeholder in every language, and shows a live preview of the current translation in the syntax of every platform.

The placeholder consistency QA check compares placeholders between languages in the stored form, so it works the same for every platform. Translation Memory suggestions that come from projects with platform-specific placeholders are shown in the stored form as well, so applying one never brings %1$s into a translation.

Routing string keys to the right platform

Not every string key exists on every platform. Tags decide which keys go into which file:

  • On upload, use the Tag keys in file option, or the tag_in_file parameter of the API and the configuration file, with one tag per platform, for example android or ios. The tag is added to every key in the file and removed from keys that are not in it, so keys shared by both platforms carry both tags.
  • On download, use the Include tags option, or the include_tags parameter, with the tag of the platform.

One repository can hold the files of several platforms. In the configuration file of the GitHub, GitLab, and Bitbucket integrations, and of the CLI, each file can carry its own file_type and tags:

config_version: 1.0
project_id: c776c33e-f428-4c91-87e1-a6a18c1554fe
upload:
  files:
    - file: android/app/src/main/res/values/strings.xml
      locale_code: en
      file_type: android_xml
      tag_in_file: # Added to every key in this file, removed from keys that are not in it
        - android
    - file: ios/App/Localizable.xcstrings
      file_type: ios_xcstrings
      tag_in_file:
        - ios
download:
  files:
    - file: android/app/src/main/res/values/strings.xml
      locale_code: en
      file_type: android_xml
      include_tags: # Only keys with this tag are written to this file
        - android
    - file: android/app/src/main/res/values-de/strings.xml
      locale_code: de
      file_type: android_xml
      include_tags:
        - android
    - file: ios/App/Localizable.xcstrings
      file_type: ios_xcstrings
      include_tags:
        - ios

Good to know

  • A translation is expected to use the placeholders of its main-language text. A placeholder that the string key does not define is still converted, under the name from the file or the next automatic name, and the placeholder consistency QA check reports the mismatch.
  • In plural string keys, the number of the plural forms, %d on Android or %lld on iOS, is mapped to the plural variable of the key, count by default.
  • String keys that name the same placeholder differently in two formats, for example {name} in an ARB file and {{user}} in a JSON file, get two placeholders. Align the names in the files, or rename the placeholder in the editor.
  • The placeholder settings of a project also apply to the AWS S3 integration, which uses the default syntax of generic file formats.

Tired of manually editing translation files?

Our platform streamlines software localization for you.

Try now for Free
Localizely logo
About
Free tools
Boring stuff
  • Privacy Policy
  • Terms of Service
  • Cookie Policy
Newsletter
Copyrights 2026 © Localizely