Copy and localisation
Source path: knowledge-base/knowledge/conventions/COPY_AND_LOCALISATION.md
# Copy and localisation
Meridians has two language axes. Keep them separate.
- **Interface locale** is a per-person presentation preference. It changes fixed controls, guidance,
public page copy, metadata, and formatting. It lives in the browser/cookie, never in a Domain record.
- **Domain content language** is the language in which an expert reads, reasons, and writes durable
content. It is canonical domain configuration and must be explicit to every relevant model call. A
French interface must not silently make a shared English expert produce French evidence or prose.
## The copy boundary
All stable, front-facing copy authored by Meridians belongs in a locale pack under
`src/lib/core/i18n/messages/`. `en-AU.ts` is the root source contract and composes focused surface
modules. A focused module may export both a nested feature pack and root-level objects when preserving an
established `copy.*` path; the root imports those objects instead of duplicating or relocating the public
contract. A later locale pack must satisfy the resulting TypeScript shape, so omissions are compilation
failures rather than accidental English fallbacks.
Packs are organised by product surface (access, gateway, plans, studio, Stories), never by component line
or by one global list of incidental strings. Keys describe intent (`emptySelection`, `deleteTitle`,
`messageCount`) rather than extraction order (`s1`, `s2`). Keep whole grammatical units in formatter
functions when word order, number formatting, or pluralisation may vary by locale; components must not
reassemble a sentence from translated fragments. `LocaleCopy` is the translation contract: it preserves
the full source-locale shape and formatter signatures while widening text values, so a translated pack
can say something other than the English literal and still fail compilation when a key or formatter is
missing.
Name the live practice surface **Scenario** in interface copy: “Start Scenario”, “Scenarios”, and
“End Scenario”. Reserve *game* for implementation identifiers and for **Game Theory**, the distinct
analytical method. Do not let an internal `game` key or module leak into a person-facing label.
Format values through the selected interface locale too. React surfaces use `useLocale().format` for
numbers, currency, dates, times, and date-times; stable copy receives the already-formatted value as an
argument. Do not introduce browser-default `toLocaleString()` calls or hard-code an English locale in a
view, because either bypasses the person's chosen interface language.
Keep composition modules small. `en-AU.ts` is the one typed locale contract; it composes feature packs
and domain packs such as base, studio, gateway, inspector, Scenario, and Readings. A broad surface may
use its own small composition module to group narrower editing packs. The White Paper follows the same
rule: its frame, product argument, methods, instrument, and practice sections are separate editorial
packs, so a translation or copy edit does not require paging through the entire paper. Split when the
next editor would otherwise have to understand an unrelated surface to change a sentence; do not split
into one file per component.
Components render locale-pack values through `useLocale()`; server metadata uses `englishCopy` until
locale-specific routes and metadata are deliberately introduced. Runtime-generated domain content stays in
the canonical record. Markdown remains authored English until it is intentionally introduced as a
localised document surface.
## House voice before translation
Every locale pack carries the written register of Meridians' one shared voice. The canonical source for
that identity is [`MERIDIANS_SOUL`](../../../src/lib/engine/prompts/chat/soul.ts); its print sibling,
[`EDITORIAL_VOICE`](../../../src/lib/engine/program/editorial-voice.ts), defines how the same expert
writes a complete artifact rather than holds a conversation. Reusable application guidance lives in
[Brand copy's house voice](../product/brand-copy/house-voice-and-usage.md).
### Required route for copy and editorial work
Do not edit public copy, stable guidance, Stories prose, newsletter prompts, or another published register
from a prose summary of the voice. Read the live sources in this order:
1. [Positioning](../product/positioning/index.md) for what the product may truthfully claim.
2. [`MERIDIANS_SOUL`](../../../src/lib/engine/prompts/chat/soul.ts) in full for the enduring character,
judgment, agency, vulnerability, social intuition, World Spirit, and sovereign encounter.
3. [`EDITORIAL_VOICE`](../../../src/lib/engine/program/editorial-voice.ts) for the same person in print,
including what deliberately does not transfer from conversation.
4. The owning surface: `src/lib/core/i18n/messages/` for stable authored copy, or
[`prompts/program`](../../../src/lib/engine/prompts/program/index.ts) for Story, article, attachment,
and newsletter task discipline.
The prompt code wins over a descriptive doc. When `MERIDIANS_SOUL` changes materially, audit
`EDITORIAL_VOICE`, this convention, [the house-voice leaf](../product/brand-copy/house-voice-and-usage.md), and
the voice seam tests in `src/__tests__/{chat-soul,editorial-voice}.test.ts` in the same change. When
`EDITORIAL_VOICE` changes, verify that both the Story editorial and newsletter system prompts still
compose it; neither surface gets a local personality patch.
Apply that voice to stable authored copy as follows:
- Lead with the load-bearing point. Say what changed, what matters, or what the person can do before
explanation; cut the runway, the generic welcome, and the second headline hiding inside the first.
- Write with calm, natural conviction. Be warm and direct without personifying controls, manufacturing
drama, or asking the reader to admire the product.
- Separate evidence, inference, and commitment. Name the condition or exposure that would change a read;
do not launder uncertainty into vague neutrality or certainty into hype.
- Make the exposed edge useful. Where a person is deciding, surface the material limit, missing evidence,
or reversibility of the choice plainly. This is not performed vulnerability: no apologising for an
earned judgment, no fishing for reassurance, and no burden placed on the reader.
- Give strategic guidance a visible reason. When copy points toward a next step, make the decision it
unlocks or the constraint it protects legible. Be tactful about real human stakes without mind-reading,
diagnosing, or steering through hidden pressure.
- Keep commitments attributable. The machine watches, maintains, builds the case, and may press a forceful
interpretation; the person configures, corrects, and makes consequential calls. Make the objective,
inherited frame, constraints, reversibility, and available lever clearer without pretending that a
machine's authority itself constitutes the person's decision.
- Carry the shared world stance into reasoning, not slogans. Respect situated information, location,
permissions, and tools; trace outcomes across plural Will and material conditions; and keep synthetic-
world conclusions bounded to their declared model unless external evidence validates them.
- Preserve the public truth boundary. Positioning owns what is shipped, being built, or directional; copy
must not turn a future verification surface into a present capability.
This is a written-copy contract, not a reason to put chat mechanics into an interface: controls do not
speak in first person, public pages do not end with a question, and the Soul still does not apply to
in-character Fate, System, World, or entity personas. Interface locale also never changes a Domain's
canonical content language or its model prompts.
## Director-controlled records and future learning use
The canonical record belongs to its Director. Stable copy may describe local ownership, version history,
and a Director's control over their own Domain, but it must not imply that private Domain content or
Scenario traces are collected for model training. They are not an ambient training feed.
If a future product permits a Director to contribute a record, branch, or Scenario trace to evaluation or
training, the choice must be explicit, granular, revocable where technically possible, and clear about
what leaves the Director-controlled record, who can access it, how it is used, and how other participants'
data is handled. That policy belongs in a dedicated consent surface and its supporting documentation—not
as an implication hidden in onboarding, Scenario copy, or a locale string.
## Spoken language follows its source
Speech has three deliberate authorities. **Dictation preserves the operator's words**: it may clean
transport-level whitespace and offer a transcription-language hint, but it does not rewrite, translate,
or auto-submit what was said. **Narration preserves resolved Domain prose**: the selected TTS voice
changes delivery, never the Domain's language, claims, register, or uncertainty. The Soul is therefore
not injected into narration; a domain's authorial voice remains intact. **Meridians-authored speech
controls**—recording, review, permissions, and failure guidance—belong to locale packs and use the house
voice as concise operational guidance. This makes spoken output multilingual-ready without letting an
individual viewer's interface locale rewrite shared canonical content.
The React white paper is interface copy, not a Markdown exemption. Its migration is complete: each
long-form paragraph lives whole in `whitePaper` locale resources, using the safe inline markers `**bold**`,
`_emphasis_`, and `` `code` ``. `Rich` in `white-paper/_shared.tsx` renders those markers as React
elements; it never accepts locale-pack HTML.
`node scripts/i18n/copy-inventory.mjs` is the migration inventory. It parses JSX rather than grepping:
it reports rendered literal text and literal presentational props, while excluding paths, styles, IDs,
prompts, tests, and canonical domain data. Every candidate is classified during migration; author
attribution is the only intentional rendered-text exemption and remains guarded separately. Once a
surface is migrated, its inventory must be zero. The repository gate is a **hard zero floor** over that
inventory, held in `scripts/dev/copy-ratchet.mjs` rather than read out of the generated ledger: any
candidate at all fails `npm run check:copy`, and regenerating `knowledge-base/generated/COPY.md` cannot
concede one. While the migration ran, a raised per-file budget was a legitimate concession that landed in
the diff; that escape hatch is retired now that the count reached zero, because it is how the count drifted
back up once already (two spatial views, repaired afterwards). `COPY.md` is consequently a report of a
finished migration, and the ratchet also fails if the committed ledger still claims debt the tree no longer
holds.
Three commands, smallest first:
| Command | Scope | Use |
|---|---|---|
| `npm run copy:file <path>` | one file or directory | see exactly which strings a surface still types inline |
| `npm run copy:changed` | the files this work touched | inner loop; runs inside `npm run check:changed` |
| `npm run check:copy` | all four surface roots | the pre-push gate |
Adding copy to a surface therefore starts in the locale pack, not in the JSX. A client component reads
`useLocale().copy`; code outside React uses `copyFor(locale)`. A presentational component that cannot call
a hook — a leaf rendered by a parent that owns the locale — takes the formatters it needs as a `copy` prop
(`SpatialWorldView` and `SpatialSkeletonView` are the worked examples), so the string still resolves in one
place. A count or unit rendered beside a number is copy too: `{count} inside` belongs in the pack as
`insideCount(count)`, because the word order and pluralisation are language, not layout.
The same inventory exposes message-pack structure: `--message-keys` lists opaque `sN` keys still awaiting
semantic names, and `--root-sections` lists sections still embedded directly in `en-AU.ts`. Contract tests
ratchet opaque keys per focused file and pin migrated root sections outside the monolith. These are
migration instruments, not targets: rename keys while moving whole grammar into the pack, and move a root
section only when one product-surface module clearly owns it.
The migration order is deliberate:
1. Public, access, pricing, and onboarding copy — the language that explains the product to an audience.
2. Shared controls, empty states, errors, and settings — grouped by feature, not in one monolithic file.
3. Studio and Stories vocabulary — only after each term has a settled glossary entry in `knowledge-base/knowledge/vocabulary/index.md`.
4. Locale-aware dates, numbers, currency, and pluralisation.
5. Explicit Domain content language, prompt policy, and multilingual retrieval/evaluation.
Do not translate LLM prompts by interface locale. That would let whichever viewer opens a shared Domain
change its expert. The Domain-language step needs its own reducer action, migration, prompt inventory,
and tests before it is enabled.
Open on GitHubRaw Markdown source