# Translation

Every player-facing string (names, labels, short labels, descriptions, group labels, choice prompts) is looked up by **key**. None is written inline next to the mechanics. Adding a language therefore never means restructuring a pack.

## Files

- One file per language per pack: `i18n/<lang>.yaml`, a flat map from key to message ([`i18n.schema.json`](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/schemas/i18n.schema.json)). Language tags are BCP 47 (`en`, `de`, `pt-BR`).
- A large pack may split a language across `i18n/<lang>/*.yaml`; the build merges them into one file per language. A key defined twice is an error.
- The file for the pack's `defaultLanguage` MUST exist and MUST contain every required key (below). Other languages may be partial.

```yaml
# i18n/de.yaml
weapons.longsword.name: Langschwert
spells.fireball.name: Feuerball
```

## Messages

Messages use **ICU MessageFormat** for placeholders and plurals, for example `"{base} +1"` and `"{count, plural, one {# charge} other {# charges}}"`.

**Markup**: a description, any key ending in `.description`, and a block (`.block`) are **CommonMark
with GFM tables**, after ICU formatting. Apps render it, or show it as plain text, which Markdown
reads as. HTML isn't allowed in a description or a block: the build rejects a raw HTML block or tag
(an autolink, `<https://…>`, isn't HTML), so apps never render a pack's HTML. Every other message is
plain text.

```yaml
spells.fireball.description: |-
  A bright streak flashes from your pointing finger to a point you choose …

  ***At Higher Levels.*** When you cast this spell using a spell slot of 4th level or higher, …
```

## Key conventions

| Key                                                | For                                                                                                                                        | Required in the default language                           |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `pack.name`                                        | The pack's display name                                                                                                                    | yes                                                        |
| `headline`, `headline.separator`                   | The character's headline, plain text, and the separator of its lists ([Headlines](#headlines))                                             | no                                                         |
| `<contentId>.name`                                 | Each content item (`weapons.longsword.name`)                                                                                               | yes                                                        |
| `<contentId>.description`                          | Optional longer text                                                                                                                       | no                                                         |
| `<contentId>.texts.<name>`                         | A named text the item's content type declares (`texts`): a spell's `material`, `trigger`                                                   | no                                                         |
| `<contentId>.state.<key>.name`                     | A state key an item or template declares                                                                                                   | no                                                         |
| `<contentId>.state.<key>.values.<v>.name`          | An enum state key's value, for a key an item or template declares; a key its type declares too falls back to the type's                    | yes, unless the type declares the key                      |
| `<contentId>.computed.<name>.name`                 | A computed value an item or template declares                                                                                              | no                                                         |
| `<contentId>.block`                                | An item's own block, replacing its type's; a template's only block ([Blocks](#blocks))                                                     | no                                                         |
| `<contentId>.headline`                             | An item's own headline piece, replacing its type's ([Headlines](#headlines))                                                               | no                                                         |
| `fields.<fieldId>.name`                            | Each field (`fields.abilities.str.score.name`); `{name}` is a named field's name (`nameFrom`)                                              | yes, unless the field is `hidden`                          |
| `fields.<fieldId>.short`                           | Short label (STR)                                                                                                                          | no                                                         |
| `fields.<fieldId>.values.<v>.name`                 | An enum field's value (`fields.creature.size.values.medium.name`)                                                                          | yes, unless the field is `hidden`                          |
| `types.<type>.name`                                | Each content type                                                                                                                          | yes                                                        |
| `types.<type>.state.<key>.name`                    | A content type's state key (`types.cohort.state.harm.name`)                                                                                | no                                                         |
| `types.<type>.computed.<name>.name`                | A content type's computed value (`types.cohort.computed.quality.name`)                                                                     | no                                                         |
| `types.<type>.block`                               | A content type's block: its items' card header ([Blocks](#blocks))                                                                         | no                                                         |
| `types.<type>.headline`                            | A content type's headline piece: one of its items in the headline's `{type_<type>_each}` ([Headlines](#headlines))                         | no                                                         |
| `types.<type>.fields.<field>.name`, `.description` | An item field's name and one-line hint (`types.weapon.fields.damageType.name`), for a custom-item form ([Item fields](#item-fields))       | yes (name), every item field, a hidden one too             |
| `types.<type>.fields.<field>.values.<v>.name`      | An item field's value: an enum's (`types.spell.fields.school.values.evocation.name`), or any other                                         | no                                                         |
| `types.<type>.state.<key>.values.<v>.name`         | A state key's value, likewise; an enum key's values are required                                                                           | yes for an enum key's values                               |
| `types.<type>.copies`                              | A content type's copy limit, for `too-many-copies` (`max`, `count` as arguments)                                                           | no                                                         |
| `templateFields.<name>.name`, `.description`       | A template field's name and hint, in each pack that declares it                                                                            | yes (name)                                                 |
| `tags.<tag>.name`, `.description`                  | A tag, in the pack that declares it (`tags.finesse.name`)                                                                                  | yes (name)                                                 |
| `groups.<groupId>.label`                           | Display groups (default label key)                                                                                                         | yes                                                        |
| `groups.<groupId>.sections.<key>.name`             | A content group's section: `groups.spells.sections.0.name`                                                                                 | no                                                         |
| `units.<unitId>.name`, `.short`                    | A display unit: "feet", "ft"                                                                                                               | yes, when a hint names the unit                            |
| `actions.<actionId>.name`                          | Rules-level actions                                                                                                                        | yes                                                        |
| `<contentId>.actions.<actionId>.name`              | Actions defined inline on a content item                                                                                                   | yes                                                        |
| `<actionKey>.options.<optionId>.name`              | Written options of a choice step (`actions.weapon-attack.options.hit.name`)                                                                | yes                                                        |
| `<actionKey>.steps.<stepId>.prompt`                | A choice step's or an allocate step's question                                                                                             | yes                                                        |
| `<contentId>.options.<optionId>.name`              | An option a content item adds to choice steps (`option` effects)                                                                           | yes                                                        |
| `types.<type>.options.<optionId>.name`             | An option a content type adds for each of its items, when the item doesn't name it                                                         | yes                                                        |
| `<actionKey>.steps.<stepId>.name`                  | A roll step's name                                                                                                                         | no; yes for the rolls of the action a creation start names |
| `<actionKey>.params.<name>.name`                   | An action parameter's label                                                                                                                | yes                                                        |
| `<actionKey>.params.<name>.description`            | A parameter's one-line hint, for its input ("The target to beat")                                                                          | no                                                         |
| `<actionKey>.params.<name>.values.<v>.name`        | An enum parameter's value (`actions.cast-spell.params.slotType.values.pact.name`)                                                          | yes                                                        |
| `<actionKey>.participants.<role>.name`             | An action role                                                                                                                             | no                                                         |
| `<actionKey>.results.<result>.name`                | A declared result (not a band set: those use `bands.<set>.<band>.name`)                                                                    | yes                                                        |
| `<actionKey>.description`                          | Optional longer text for an action                                                                                                         | no                                                         |
| `fields.<fieldId>.description`                     | Optional longer text for a field (a tooltip)                                                                                               | no                                                         |
| `situational.<id>.name`                            | Situational choices                                                                                                                        | yes                                                        |
| `situational.<id>.values.<v>.name`                 | An enum situational choice's value (`situational.bonusDie.values.devils-bargain.name`)                                                     | yes                                                        |
| `bands.<set>.<band>.name`                          | Result bands                                                                                                                               | yes                                                        |
| `bands.<set>.<band>.condition`                     | How a band's tests read in a roll summary; ICU `value`, then `value1`, `value2`, … per test                                                | no                                                         |
| `tallies.<name>.name`                              | Roll tallies                                                                                                                               | no                                                         |
| `links.<slot>.name`                                | Link slots                                                                                                                                 | no                                                         |
| `followUps.<id>.name`, `.description`              | Rules follow-ups, derived and ephemeral offers alike; on content `<contentId>.followUps.<id>.…`, on a type `types.<type>.followUps.<id>.…` | no                                                         |
| `<owner>.choices.<choice>.prompt`                  | Choice prompts; `<owner>` is the content ID, or `rules` for rules-level choices                                                            | yes                                                        |
| `<owner>.choices.<choice>.options.<id>.name`       | Inline options; a value choice's values, `<id>` the value (optional)                                                                       | yes                                                        |
| `<owner>.prerequisites.<id>`                       | Prerequisite text ("needs Str 13")                                                                                                         | yes                                                        |
| `creation.<id>.name`, `.description`               | A creation start; both required, the description one sentence                                                                              | yes                                                        |
| `constraints.<id>`                                 | Constraint text ("at most 3 attuned items")                                                                                                | yes                                                        |

`<actionKey>` is an action's key root: `actions.<actionId>` for a rules action and
`<contentId>.actions.<actionId>` for an action defined inline on an item. A paused action and
each log entry name their defining action (`action`), so an app builds these keys for nested
actions too ([engine-actions.md](https://opencharacterformat.org/spec/engine-actions/#checkpoints-and-the-policy)); `<stepId>` is the
last segment of the step's path.

A choice step over content or copies has no option keys of its own: each option
is named by its item's name, `<contentId>.name` (a copy by its item's, or by its own name from
its state, computeSheet's `copyNames`). The reserved option
`#done`, which ends a repeated choice step, is named by `actions.options.done.name`, standard
text the spec ships in [`../i18n/en.yaml`](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/i18n/en.yaml) for every pack.

A choice step over pending choices has no text of its own: each pick is named
by the choice it asks, `<owner>.choices.<choice>.prompt` with `needs.choice.owner`, and its
options as `listOptions` results are. Its `#done` ends the whole step, leaving every choice still
pending, so it is named by `actions.options.done.pending.name` ("Skip the rest").

A slotted choice has one prompt for all its slots, under its declared ID, with
the slot's number as the ICU argument `{slot}`:
`classes.fighter.choices.class-feat.prompt: 'Class feat ({slot, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} level)'`.
A pending slot and `needs.choice` carry `slot: { choice, number }` for the app to find the key
and fill the argument; options and nested choices are keyed under the declared ID too. The
build warns about a slotted choice's prompt that doesn't use `{slot}`.

An option content adds to a choice step (an `option` effect) has the ID
`<qualified content id>#<option id>`; it is named in the content's pack by
`<contentId>.options.<optionId>.name` (`special-abilities.battleborn.options.special-armor.name`),
whatever action it is offered in, so each item can say which ability allows it and when. A
content type's option has its item's ID too, and is named by the item's key
when the item has it, else by `types.<type>.options.<optionId>.name` in the type's pack.

An explicit `label`, `shortLabel` or `prompt` in the mechanics overrides the default key (a roll
step's `label`, a choice step's `prompt` and a parameter's `label` too). An explicit key is
always required. Keys are permanent, like IDs.

An outcome's `data` keys have no names: they're mostly numbers and flags (`attack: 22`,
`critical: true`) that an app formats itself.

**Named texts**: a content type may declare `texts: [<name>, …]`. Each item of
the type may have `<contentId>.texts.<name>`; none is required, and apps know from the type which
to look up and where to show them (a spell's material component and a reaction's trigger, in
its header):

```yaml
# rules: contentTypes.spell.texts: [material, trigger]
spells.raise-dead.texts.material: a diamond worth at least 500 gp, which the spell consumes
spells.shield.texts.trigger: which you take when you are hit by an attack or targeted by the magic missile spell
```

**State keys and computed values** are named by
`types.<type>.state.<key>.name` and `types.<type>.computed.<name>.name` when the content type
declares them, and `<contentId>.state.<key>.name` and `<contentId>.computed.<name>.name` when an
item or template does. A name an item's declaration replaces falls back to the type's key. An app
may leave out a state key or computed value without a name. An enum state key's values are named,
`types.<type>.state.<key>.values.<v>.name` for a type's key and
`<contentId>.state.<key>.values.<v>.name` for an item's own (Fate's custom stunt's `limit`), both
required in the default language; an item's value falls back to its type's when the type declares
the key too.

**Tags** have a name and an optional description, `tags.<tag>.name` and
`tags.<tag>.description` (`tags.finesse.name`), in the pack that declares the tag, as a
field's text is in the pack that declares the field. The name is required for every declared
tag, a declared tag's parents included (declaring `save.int` declares `save`), so an app can
name a tag stored as a value (a set field's member). A pack that only uses another pack's tag
doesn't name it.

**Bands:** a roll's log entry and its step result carry `bandSet`, the qualified ID of the band
set it was classified by, next to `band`, so an app builds `bands.<set>.<band>.name` (with the
set's unqualified ID, in the set's pack) without reading the action
([engine-actions.md](https://opencharacterformat.org/spec/engine-actions/#roll-steps)). A roll's summary lists the
bands it can still reach, each with structured `tests`; an app words them with the band's
optional `bands.<set>.<band>.condition` text (`value` is the first test's number, `value1`,
`value2`, … each test's in order), else the spec's `compare.<code>` text per test
([engine-actions.md](https://opencharacterformat.org/spec/engine-actions/#the-roll-summary)).

Unused keys in the default language are warnings, not errors. A named text a content type
doesn't declare, text for a tag the pack doesn't declare, and a value name for an undeclared
enum value are unused keys.

Template names receive the base item's translated name as the ICU argument `{base}`: `magic-items.weapon-plus-one.name: '{base} +1'`. (ICU argument names cannot contain dots, so it is `{base}`, not `{base.name}`.)

## Item fields

An item field is the item's own data, written by whoever writes the item (a pack's author, or a
player making a custom item): the engine never changes it, and whatever changes on a copy is a state
key. So every item field is author-facing, and each is named, `types.<type>.fields.<field>.name`,
with an optional one-line hint, `.description`. An app building an "add a custom item" form from a
content type's `fields` labels each input with them (`text.typeField`, `text.typeFieldDescription`),
and saves the item with `setHomebrew` and `addItem`. The display hint `hidden` on an item field
keeps it off the item's card, not out of the form: a form asks for every field, and may fold hidden
and optional ones under "more". A custom template's form names the rules' template fields by
`templateFields.<name>.name`. An action's inputs get hints the same way: a parameter's optional
`<actionKey>.params.<name>.description` (`text.paramDescription`).

```yaml
types.weapon.fields.versatileDamage.name: 'Two-handed damage'
types.weapon.fields.versatileDamage.description: 'Damage with the versatile property, used in two hands.'
templateFields.acBonus.name: 'AC bonus'
actions.test.params.dr.description: 'The target to beat. 12 for most rolls; the GM raises or lowers it.'
```

## Blocks

A **block** words a content item's card header from its mechanics, for an app to
show above the description: `types.<type>.block` for every item of a type, `<contentId>.block`
for one item (it replaces the type's). It is an ICU message, then Markdown, a line or a few;
the description stays the item's text.

```yaml
types.spell.block: >-
  **{name}** — *{level, select, 0 {{school_name} cantrip} other {Level {level} {school_name}}}*
  · {castingTime_name} · {rangeKind, select, ranged {{range} ft} other {{rangeKind_name}}}
  · {verbal, select, true {V} other {}}{somatic, select, true {{verbal, select, true {, } other {}}S} other {}}
  · {durationUnit_name}
types.spell.fields.school.values.evocation.name: 'Evocation'
types.spell.fields.castingTime.values.action.name: '1 action'
# Fire Bolt: **Fire Bolt** — *Evocation cantrip* · 1 action · 120 ft · V, S · Instantaneous
```

A template's arguments:

| Argument                | Value                                                                                                                                                                                                                 |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{name}`                | The item's name; a copy's, with its templates                                                                                                                                                                         |
| `{<field>}`             | An item field the type declares: a copy's own field, else the item's, else the type's default. Numbers and booleans as they are; a set is a list                                                                      |
| `{<field>_name}`        | The value's name: its `types.<type>.fields.<field>.values.<value>.name` (an enum's, or any value's: orc-2e's light Bulk `0.1` is `L`), else a content ID's item name, a tag's name, else the value; a set's as a list |
| `{tags}`, `{tags_name}` | The item's tags, and their names, as lists                                                                                                                                                                            |
| `{texts_<text>}`        | A named text the type declares (`{texts_material}`)                                                                                                                                                                   |
| `{self_<key>}`          | A copy's state key or computed value, when the app gives a copy; `{self_<key>_name}` its value's name (the item's own `<contentId>.state.<key>.values.<value>.name`, else the type's)                                 |
| `{tag_<tag>}`           | `true` when the item has the tag, or one below it, else `false`. The tag is spelt with each `.` and `-` as `_`, since ICU argument names can't hold them: `{tag_trait_cantrip}`                                       |
| `{has_<argument>}`      | `true` when any argument above has a value (not absent, not empty), else `false`                                                                                                                                      |

- **Precedence:** `name`, `tags`, `tags_name` and `texts_*` first, then the type's declared
  fields, then the `has_`, `tag_`, `self_` and `_name` forms.
- **A tag test** picks a wording by trait: orc-2e's spell header reads "Cantrip 1" for a cantrip,
  `{tag_trait_cantrip, select, true {Cantrip} other {Spell}} {rank}`. Two declared tags that
  spell the same (`a.b` and `a-b`) can't be told apart; a pack avoids declaring both.
- **Templates** have no content type, so a template's block is its own
  `<contentId>.block`. Its fields are formulas over the item it's applied to, so only the ones
  written as a number or a boolean are arguments (`{potency}`); `{name}` is its name on its
  `appliesTo` type's (`Flaming {base}` on a weapon is "Flaming Weapon"); `tags`, `tags_name`,
  `tag_` and `has_` work as for items. orc-2e's runes:
  `runes.flaming.block: '**Flaming rune** — *Rune 8* · {tags_name} · 500 gp'`.
- **Missing values** are the empty string: they print nothing and a `select` takes `other`. A
  `plural` of nothing isn't zero, so a part that needs a value tests it first:
  `{has_maxCharges, select, true { · {maxCharges} charges} other {}}`.
- **Lists** (a set, tags) are joined with the language's list format.
- **What ICU can't do** is the pack's: a dice field prints in dice notation (`{damage}`, 8d6),
  a unit is a word in the template, letters are a `select`. A display value the template can't
  build is a field or computed value the pack adds, not template syntax.
- **Value names** are text in their own right, so apps can use them outside a block (a filter
  or a column); write them as they read best in the pack's blocks. A template that needs another
  wording writes a `select`.

An app shows an item's **block, then its description**, separated by a blank line; either
alone when the other is missing. `@opencharacter/text` gives that as `contentCard` and the block
alone as `contentBlock` (a block that doesn't format is no block).

`pnpm validate` checks a block: it parses as ICU, has no HTML, and names only arguments its type
gives (its declared fields, named texts, state keys and computed values; for `<contentId>.block`
also the item's own; a declared tag for `tag_`; for a template its literal fields). A `_name` of an enum of the pack's own type needs a name for each value in
the default language.

## Headlines

A **headline** is a one-line description of a character, for an app to show
under its name in a list: "Level 3 Hill dwarf Wizard", "Bruiser of the Ravens", "Tier 1".
A pack words it as `headline`, an ICU message, **plain text** (no Markdown), filled from the
character's sheet. It is optional: without one, or while it words nothing (a sheet with nothing
picked yet), an app shows its own fallback, such as the pack's name.

```yaml
# 5e-srd-5.1
headline: >-
  {has_type_monster, select,
    true {{type_monster}, CR {field_challenge_rating_text}}
    other {{type_class_count, plural, one {Level {field_level_total}} other {}}
      {has_type_subrace, select, true {{type_subrace}} other {{type_race}}}
      {type_class_count, plural, one {{type_class}} other {{type_class_each}}}}}
headline.separator: ' / '
types.class.headline: '{name} {self_level}'
# "Level 3 Hill dwarf Wizard", "Hill dwarf Wizard 2 / Fighter 1", "Goblin, CR 1/4", "Dwarf"
```

A headline's arguments:

| Argument               | Value                                                                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{field_<field>}`      | A field's value, its ID spelled with `_` for `.` and `-` (`field_level_total`, `field_aspects_highConcept`), as a tag test spells a tag; a set is a list                     |
| `{field_<field>_name}` | The value's name: a content item's, a field's or a tag's name, else the value                                                                                                |
| `{field_<field>_text}` | The value as the sheet shows it: a number in its display hint's `format` (`signed`, `percent`, `fraction`: "1/4") and with its `unit`'s short name; else as `_name`          |
| `{type_<type>}`        | The names of the character's content of a type, as a content group of the type lists it: each copy (with its templates) and each picked or granted item, in activation order |
| `{type_<type>_count}`  | How many there are                                                                                                                                                           |
| `{type_<type>_each}`   | Each worded by its headline piece (below), else by its name                                                                                                                  |
| `{link_<slot>}`        | The name of the character filling a link slot, which the app gives (the format doesn't name characters)                                                                      |
| `{has_<argument>}`     | `true` when the argument has a value (not absent, not empty), else `false`, for `select`                                                                                     |

- **Which headline:** the last of the character's packs (in its `packs` order, homebrew last)
  that has `headline`. A field or content type is looked up in that pack first, then in the
  character's other packs in order.
- **Missing values** are the empty string, as in a block: a part that needs a value tests
  `has_` first. A count of none is `0`.
- **Lists** are joined by the pack's `headline.separator` (" / "), else the language's list
  format ("Wizard, Fighter").
- **Spaces:** the result is trimmed and each run of white space is one space, so a template
  may leave a missing part's space in place ("{type_race} {type_class}" is "Dwarf" before a
  class).
- **A headline piece** words one item for `{type_<type>_each}`: `types.<type>.headline` for
  every item of a type, in the type's pack, or `<contentId>.headline` for one item (it replaces
  the type's: MÖRK BORG's "No class" item is "Classless"). Its arguments are a block's
  ([Blocks](#blocks)): `{name}`, the item's fields, and for a copy its state and computed values
  (`{self_level}`). Plain text, like the headline.

`pnpm validate` checks a headline: it parses as ICU, and each argument names a field, a content
type or a link slot the pack sees (one field per spelling). A headline piece is checked as a
block is.

## Packaging

- **Two packages per pack:** the mechanics package also contains the default language, and one `-i18n` package holds all other languages. Translation fixes never change the mechanics pack's version.
- Text isn't saved state, so it isn't pinned. Because keys are permanent, the latest translations work with older pack versions.
- Every language, including the default, is its own entry point, so apps ship only the languages they import.
- **Fallback:** a missing translation falls back to the default language, if the app has loaded it.
- Translating copyrighted text is still copying it, so a pack's license applies to its translations too.

How the build packages translations (per-language entry points, the `-i18n` package) is in
[packs.md](https://opencharacterformat.org/spec/packs/#releases).

## Engine output

The engine never outputs display text. It outputs keys, codes and data, and the app renders them in the player's language. The spec ships standard English text for its own error and rules-issue codes in [`../i18n/en.yaml`](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/i18n/en.yaml), under `errors.<code>` and `issues.<code>`, with placeholders filled from the error's `data`, for the options it reserves (`actions.options.done.name`), for a roll summary's comparison codes (`compare.lt`, `compare.le`, `compare.gt`, `compare.ge`, `compare.eq`, with the placeholder `{value}`), and for a roll's dice: `dice.dice` (`{count}`, `{sides}`) and `dice.fate` for a dice item, one `dice.<modifier>` per entry of [`dice-modifiers.json`](https://github.com/pixelqueue/opencharacter/blob/main/packages/spec/data/dice-modifiers.json) (`{count}`, `{target}`, or `{faces}`, the faces it matches as `compare.<code>` text or the face itself), added with `dice.modified`, and items joined with `dice.plus`, `dice.minus` and `dice.negative`. Every code in the fixed sets has its text; `errors.other` (`{code}`) words a code an app has no text for (an engine newer than its text), so a player never sees a bare code.

Turning keys into text is the app's. The reference helper `@opencharacter/text`
builds every key above from engine output and formats it; it is not part of conformance.

The spec package exports that text as packs export their default language
([packs.md](https://opencharacterformat.org/spec/packs/#releases)): `@opencharacter/spec/i18n/en` is a JavaScript module whose default
export is the flat map of keys to messages, with the same map as JSON for other runtimes (`require`,
or `dist/i18n/en.json` in the package). The YAML source stays available as
`@opencharacter/spec/i18n/en.yaml`.
