# Placeholders, and why they are stored separately

**Category:** [String Manager](https://help.totalctrl.app/hc/totalctrl/totalctrl-help-center/en-US/categories/strings)
**Updated:** 2026-10-04

## One string, seven spellings

The same sentence arrives written seven different ways depending on where it came from:
Where it came fromHow it is written
iOS`Welcome %@! You are a %@ customer.`
Android`Welcome %s! You are a %s customer.`
gettext`Welcome %(name)s! You are a %(tier)s customer.`
ICU / Flutter`Welcome {name}! You are a {tier} customer.`
i18next`Welcome {{name}}! You are a {{tier}} customer.`

Stored as they arrive, those are five unrelated strings. A translation memory cannot match them, a duplicate check cannot see they are the same sentence, and no analysis can tell you whether a translation kept all the values the source had.

## What is stored

On upload, every string is **normalized** to one canonical form — `Welcome {name}! You are a {tier} customer.` — and three things are kept:

- **The normalized value.** This is what you edit, translate, search and compare.

- **Each placeholder on its own** — its canonical name, the exact token it came from, whether it holds text or a number, and *which argument it is*.

- **The original string, untouched.** Nothing is thrown away, so nothing has to be reconstructed from a guess.

On download, each placeholder is written back in the target format's own spelling. Send the same string to an iOS build and it comes back as `%@`; send it to a .po file and it comes back as `%(name)s`.

## Reordering, which is the part that matters

Word order changes between languages, and a translator will move a value to where it belongs. That is fine in some formats and quietly broken in others, because `%s` does not mean "the customer's name" — it means **the next argument**. Swap two of them in a translation and the runtime still hands over the arguments in the original order, so your app greets somebody by their subscription tier.

String Manager knows each placeholder's argument position, so on download it can tell the difference:

- If the translation uses every placeholder exactly once and in the source's order, the simple spelling is safe and is what you get: `%s`, `%@`.

- If anything has moved, repeated or been dropped, the **explicit** spelling is written instead — `%1$s`, `%2$@` — which names the argument rather than counting to it. The translation is delivered as written and it binds to the right value.

You do not choose this and there is no setting for it. It is decided per string, per language, from what that translation actually says.

## Detected formats

iOS and Android printf (`%@ %d %1$s`), gettext (`%s`, `%(name)s`), Python (`{name}`, `{0}`), ICU MessageFormat including plurals, and i18next (`{{name}}`, `__name__`). A string with no placeholders is stored as it stands.

## Where you see it

Open any string. The **Placeholders** list shows each one with its original token and its position. The consistency check in **Run analysis** compares that list against every translation and reports a placeholder a translation added, dropped or altered — the class of bug that otherwise ships and crashes on a device.

---

## Related Articles

- [API, webhooks, and automation](https://help.totalctrl.app/en-US/articles/string-manager-integrations)
- [Platform keys: one string, the name each build expects](https://help.totalctrl.app/en-US/articles/string-manager-platform-keys)
- [Download the file you uploaded, with the values changed](https://help.totalctrl.app/en-US/articles/string-manager-template-export)
- [Find: duplicates, old terms, blocked words and tone](https://help.totalctrl.app/en-US/articles/string-manager-find-duplicates)
- [Create a string set and add languages](https://help.totalctrl.app/en-US/articles/create-a-string-set)

---
[← Back to TotalCtrl Help Center](https://help.totalctrl.app/en-US/)