Engineeringこの記事を日本語で読む

Placeholder styles in game text: a tour, and why to pick just one

Almost every line of game text has a hole in it: a player name, a number of items, a damage value. That hole is filled at runtime by a placeholder token embedded in the source string. The syntax used for that token varies a lot between engines, frameworks, and hand-rolled string systems, and it is worth knowing the common ones before you pick — or inherit — one for your project.

printf-style: %s, %d, %1$s

Inherited from C's printf family, this style embeds a type code directly in the token. %s takes a string, %d takes an integer, and so on. Plain %s/%d tokens are positional by order of appearance in the string, which means a translator has no way to change the argument order without the runtime silently mismatching values.

Some implementations support explicit positional indices, letting a translator reorder arguments to match target-language grammar without the runtime reordering them itself:

You picked up %1$s x%2$d
アイテム「%1$s」を%2$d個手に入れた

Indexed: {0}, {1}

A simpler positional style, common in .NET-style formatting and many custom engines. No type code is encoded in the token — the type is inferred at the call site. Reordering ({1} before {0} in translation) is usually supported directly, since the index itself carries the position, which is exactly what plain %s/%d cannot offer.

{0} dealt {1} damage to {2}

Named: {playerName}, {itemCount}

Named placeholders trade brevity for clarity. A translator working from {playerName} defeated {enemyName} does not need to guess what argument 0 versus argument 1 means — the name documents it. This matters more as string count grows and reviewers stop having source-code context for every line, since the string itself now carries enough information to review in isolation.

{playerName} defeated {enemyName} in {turnCount} turns

ICU MessageFormat: plurals and gender built in

ICU MessageFormat is a standardized syntax that goes beyond simple substitution — it can select text based on plural rules or gendered forms, evaluated per locale. A basic plural pattern looks like this. Many locales need more than a singular/plural split (some languages have three or more plural categories), and ICU's plural rules handle that per locale without the calling code needing to know the details.

{itemCount, plural,
  one {# item}
  other {# items}
}

Engine-style custom tags

Many game engines and dialogue systems use their own bracket or angle-bracket tokens, sometimes mixing substitution with inline formatting or rich-text markup in the same token space, e.g. <color=red>{damage}</color> or [player]. These are the hardest to standardize on because they are project-specific and rarely documented outside the engine's own reference, and a translator moving between two projects has to relearn the convention from scratch each time.

Why one style, documented, beats a mix

Mixing styles across files is where things go wrong. A translator who has only ever seen {0} style will not intuitively know that %1$s requires keeping the argument index attached, or that ICU plural blocks are not free text to rewrite. Every style above is a legitimate choice; the risk is not in which one you pick, it's in picking more than one without telling anyone.

Standardize on a single placeholder style per project, and write it down where translators actually look: a short reference sheet with one example per token type, not a paragraph buried in an onboarding doc. A translator who knows the rule for {itemName} will get it right in string 1 and string 10,000. A translator guessing at a style they've never seen will get it wrong exactly when you can't easily check — deep in a spreadsheet, far from the game.

  • Pick one placeholder style for the whole project, not per file or per feature
  • Document each token type with a real example, not just a name
  • Note whether your runtime supports positional reordering — if not, say so explicitly, since translators from other projects may assume it does
  • Keep the reference next to the export file translators actually open, not in an internal wiki they don't have access to

Related articles