---
id: authoring/messages
title: Messages & Keys
---

The core rule: **the default locale is the schema**. Its files define every namespace's key set and key order. Every other locale is **values-only** — it holds translations, never structure.

## Adding a key — edit the default locale only

```jsonc
// locales/en/common.json (default locale)
{
  "greeting": "Hello, {name}!" // ← add here
}
```

On the next dev refresh the generator auto-completes the new key into **every** supported locale:

- A missing key inherits its value from the **nearest registered chain ancestor** (`ar-SY-x-lattakia` → `ar-SY` → `ar` → default), so `"greeting": "Hello, {name}!"` lands in `ar`/`ar-SY`/`ar-SY-x-lattakia` too, and you translate each one. Chain semantics: [Dialects](./dialects.md).
- Files are re-serialized in the default locale's key order (one key per line), so a key sits on the same line number in every locale's file.
- Shape is preserved through the rewrite: both the plain string and the rich object form.

**Never add a key to a non-default locale directly** — a key present in a locale but absent from the schema is an *orphan*: appended after the schema keys, reported as a warning, and given no typegen entry.

## Translating into a non-default locale

```jsonc
// locales/ar/common.json — translate the value, keep the placeholders
{
  "greeting": "مرحباً، {name}!"
}
```

- A locale file may be **partial or absent**; completion fills the rest from the chain. You only ever write what differs.
- Keep `{param}` placeholders and the full ICU structure intact — translate the strings inside the branches, never the selector names or param names. ICU details: [ICU Select & Plural](./icu-select-plural.md).
- Keep the rich object shape identical to the default locale's:

```jsonc
// en: "confirm": { "value": "Delete?", "description": "Destructive action", "maxLength": 40 }
// ar: "confirm": { "value": "حذف؟", "description": "إجراء لا يمكن التراجع عنه", "maxLength": 40 }
```

## Message shapes

Each value is either a plain string or a rich object `{ value, description?, maxLength? }` — `description` is translator context, `maxLength` a UI constraint. Both drive tooling, not runtime.

## Deleting and renaming keys

- Delete/rename in the **default locale**. The key disappears everywhere; renamed keys behave as add + remove. A stale key lingering in a non-default locale is reported as an orphan.
- Don't delete from non-default locale files to "hide" a string — the schema still owns the key and completion will refill it.

## Verification

- The generated `stift.gen.d.ts` narrows keys/params/select categories to your real messages — compile errors are the fastest authoring feedback.
- The devtools panel ([Devtools](../tooling/devtools.md)) shows the cross-locale table and missing-key/param logs; a missing key/param logs `{ code: 'missing-key' | 'missing-param', … }` through the runtime hooks.
- `filledKeys`/`orphanKeys` surface in generator warnings — treat orphan warnings as bugs.

## Common mistakes

- Adding a key to a non-default locale only → orphan + warning, no typegen entry.
- Translating the default locale (it's the schema — always English/source).
- Reordering keys by hand in a locale file → the generator rewrites in default order; keep diffs to values only.
- Renaming a param in one locale but not the default → the union rule makes it required everywhere (see [ICU Select & Plural](./icu-select-plural.md)).

## Next

- Branching messages on gender, count, politeness → [ICU Select & Plural](./icu-select-plural.md)
- Overriding values per dialect → [Dialects](./dialects.md)
- Rendering messages with values, rich text, markdown → [Translating & Values](../react/translating.md)
