<!-- source: openspec/changes/add-muse-translation-mode/design.md -->

# Design — add-muse-translation-mode

> Companion to proposal.md. Records `D-MTM-N` and the contracts the
> implementation must honour.

> **⚠ Schema-rename overlay — RAP-1170 (Wave 1, 2026-05-22).** This file was drafted against the pre-rename `canvas_*` table names. The active rename is `canvas_sessions → ideas`, `canvas_messages → messages`, `canvas_files → files`, `canvas_invitees → spec_invitees`. The other `canvas_*` tables referenced incidentally below (e.g. `canvas_attachments`) were dropped on 2026-05-22 — see `openspec/current/db/entities.md § Project → spec → idea hierarchy`. Inline `canvas_*` references are deliberately left unmodified to preserve drafting context per the overlay note in `proposal.md`.

## D-MTM-1 — Translation provider: Anthropic-native

Translation is a system-prompt-shaped subroutine of the existing
Anthropic Claude API call:

* No new vendor; same `ANTHROPIC_API_KEY` already in use.
* Same model the canvas is configured for (Opus / Sonnet — translation
  is comfortably within Sonnet's range).
* Implemented as a new `intent: 'translation'` branch in the Architect
  runner (mirroring `intent: 'onboarding'` from RAP-1092).

**Why not a dedicated translation API (DeepL, AWS Translate)?** Three
reasons:
1. Cost — Claude tokens are already paid; adding a vendor adds a
   billing surface.
2. Vocabulary respect — D-MTM-5 forces brand-vocabulary terms into the
   translation. Anthropic prompts honour vocabulary forcing better than
   off-the-shelf translation APIs do.
3. Audit — every translation already runs through the studio's existing
   Forge audit + cost-cap surface. New vendor = new audit surface.

**Why not on-device?** Defer until proven the API cost is material.
Today's expected translation volume (10-100 turns / canvas × tens of
active canvases) is negligible against the existing chat-turn API spend.

## D-MTM-2 — Surface: inline per message

Each chat message renders the writer's original text by default. A small
disclosure widget below the message ("Show in English" / "Показать
на русском") swaps the rendered text for the translated view. State is
per-viewer, per-session — not persisted across canvases.

**Why not side-by-side pane?**
* Layout complexity: side-by-side breaks on mobile and consumes vertical
  space.
* Cognitive load: forcing the reader to scan two columns when they
  usually only need one.
* Opt-in toggle keeps the default mobile/desktop layout unchanged.

**Promote to side-by-side when:** a real engagement reports the inline
toggle is insufficient for tight-loop pair-programming sessions.
Tracked as a follow-up; no v1 work.

## D-MTM-3 — Persistence: both, translation lazy

Two storage layers:

```sql
-- D-MTM-4 — original locale
ALTER TABLE canvas_messages
  ADD COLUMN source_locale text;

-- D-MTM-3 — lazy translated views, keyed by target locale
ALTER TABLE canvas_messages
  ADD COLUMN translated_content jsonb;
  -- shape: { "en": "...", "ru": "...", "ro": "..." }
  -- null when no translation has been requested
  -- write-once per locale; never mutated after initial population
```

**Population:**

1. Original `content` is written when the user sends a turn — verbatim,
   in the writer's `source_locale`.
2. When a viewer with a different working locale clicks the "show in
   X" toggle, the chat surface calls Muse with
   `intent: 'translation', source_locale: <writer's>, target_locale:
   <viewer's>`. The translated string is written into
   `translated_content[<viewer locale>]`.
3. Subsequent viewers in the same target locale read from the persisted
   map; no re-translation cost.

**Immutability:** once `translated_content[locale]` is populated, it is
never rewritten. A user who disagrees with a translation comments inline
on the canvas as a normal turn; the chain of normal turns is the
correction record.

**Why not server-side eager translation?** Wastes tokens on languages
no viewer will request. Lazy population pays only for translations
actually consumed.

## D-MTM-4 — `source_locale` column

```sql
ALTER TABLE canvas_messages
  ADD COLUMN source_locale text;
  -- ISO 639-1 lowercase: 'en', 'ru', 'ro'
  -- (HE removed 2026-05-16; see D-MTM-7)
  -- NULL = legacy row (pre-RAP-1131)
  -- New writes always populate from session.intake_locale
  --   (RAP-1085) or session.user_locale fallback
```

`source_locale` is the indispensable join key — `translated_content`
without `source_locale` cannot be interpreted (we'd have to guess the
language from the body, which fails on short / multilingual / code
turns).

## D-MTM-5 — Brand vocabulary forcing

The translation system prompt template injects the brand's
`client_vocabulary` block as forced-term mappings:

```
# VOCABULARY FORCING

The following terms MUST be translated as specified, regardless of
LLM-default rendering. The mappings come from the canvas's brand
vocabulary (`openspec/brands/<slug>/identity.md § Vocabulary`).

{client_vocabulary_block}
```

**Sourcing:** `<brand>/identity.md § Vocabulary` is read at translation
turn time via the same `openspec/brands/<slug>/` loader pattern
RAP-1126 (`add-brand-mirroring-schema`) reserved. Until that loader
ships, the translation prompt receives an empty vocabulary block and
falls back to default LLM translation.

**Format inside `client_vocabulary`:** a markdown bullet list per term
+ per-locale mapping:

```markdown
- **compliance** — EN: "compliance" · RU: "комплаенс" · RO: "compliance"
- **brief** — EN: "brief" · RU: "бриф" · RO: "brief"
```

Free-form prose around the bullets is allowed; the prompt is a
human-readable section, not a parser target. The LLM treats each bullet
as a hard mapping.

## D-MTM-6 — Opt-in flag

```sql
ALTER TABLE canvas_sessions
  ADD COLUMN translation_enabled boolean NOT NULL DEFAULT false;
```

The chat surface checks `translation_enabled`:
* When false (default), no "show in X" toggle renders; the surface is
  identical to today's.
* When true, every message row gets the toggle when the viewer's locale
  differs from `source_locale`.

**Flipping the flag:** founder toggles in canvas settings; the toggle
is persistent. No retroactive translation — already-sent messages still
need a viewer-click to populate `translated_content`.

**Per-participant override:** out of scope. The flag is per-canvas.
A specialist who can read both languages simply doesn't toggle.

## D-MTM-7 — Locale set v1: RU / EN / RO (no HE)

```ts
export const TRANSLATION_LOCALES = ['ru', 'en', 'ro'] as const;
export type TranslationLocale = (typeof TRANSLATION_LOCALES)[number];
```

Hebrew is deliberately excluded from v1. HE was removed from the
studio's chrome locale set on 2026-05-16 (`rap_893_complete.md`,
RAP-893 archive). The prior shipment of HE was AI-drafted with no
native-speaker review and was retracted same day. The RTL-capable
design-system primitives (text-direction-aware components, logical CSS
properties) are kept; the gate is locale-policy, not technical.

**When HE returns:** the studio commits to ship HE only after a
native-speaker review pass against the chrome surface. The translation
mode adds HE to `TRANSLATION_LOCALES` in a separate change at that
point; the trigger is the native review, not technical readiness.

**Why not just three? Why not five?** Three covers the founder/client
languages actually present in current engagements (RU founders, RO
founders, EN specialists). Adding a fourth before a real engagement
needs it adds maintenance cost without proven value.

## Open questions — all closed at proposal time

* **Q-MTM-1 (closed):** Translate at write or read time? **Read time
  (lazy).** D-MTM-3 — pays only for translations actually consumed.
* **Q-MTM-2 (closed):** Add a new Muse mode (alongside `architect`),
  or a new intent within `architect`? **New intent.** Translation is
  a turn-level capability, not a separate persona — same prompt
  scaffold + a translation addendum mirrors RAP-1092 (`onboarding`)
  cleanly.
* **Q-MTM-3 (closed):** What about replies inside a translation
  context — does the founder type in source-locale or target? **Source
  always.** Every writer types in their own working language; the
  surface translates for each reader. Mixing the input language
  per-turn is friction.
* **Q-MTM-4 (closed):** Hebrew / RTL support? **No in v1 — RU/EN/RO
  LTR-only.** HE was removed from the studio chrome locale set on
  2026-05-16 (RAP-893 archive — AI-translated chrome with no native
  review). RTL-capable DS primitives are preserved for a future
  properly-translated locale. See D-MTM-7 for the canonical decision.
* **Q-MTM-5 (closed):** What about non-text content (code, links,
  numbers)? **Preserved verbatim.** Translation prompt is instructed to
  pass code fences, URLs, and numerics through unchanged. Code review
  comments still read the same code regardless of language.
