Contents

Translating what the core composes

Studio speaks the language a person chooses (Settings › Appearance › Language). Its own words go through RwLang.t, and the core's messages, written whole in the core's source, are found in the catalog by their English (RwLang.core). This note is about the rest: the sentences the core composes from pieces as it goes, which no catalog entry can match whole.

What is composed

  • The reading's staged lines (core/src/studio/reading.ts): what the director stages for a paragraph or a line, "Otto says it to Chief, excited, loud", "Chief walks up to the eng console, and sits down". Each is built from a cue: a verb, who, where, how, nested pieces.
  • The timeline's business (episodeActions): the same phrases, on the Action lanes and the storyboard's cards.
  • A scene's opening, its time and its place in the reading: "Otto at the door mark, sitting", "a flashback to 1985, on the move", "Marigold - Bridge, intercut with Galley".
  • Directions and shot lines the director reads in the script (core/src/director/skeleton.ts, written-words.ts): "a dissolve into sc02", "a close-up on Otto", "montage shot 1 of 2: a single on Chief".
  • A preview's caption (core/src/studio/preview-words.ts): "Mug · held by Otto, then set down, in 2 states · Flat", "Pilot · storyboard · 12 shots in 3 scenes", "Otto · model sheet · Flat".
  • Where a document is used, as Studio asks before deleting it: "the gag “The kettle hiccup”", "the cast", "line 12".
  • The director's notes that name a gesture or a shot's size: "Otto's wave played outside the frame of a close-up: the shot is widened to a medium shot to show it."
  • The chat's suggestion chips (core/src/showrunner/suggestions.ts), which chat.focus answers for the page the person is on: "Give Otto a winter costume", "Warm up the Marigold - Bridge for a night scene". A chip puts its words, in the person's language, in the message box; Claude's studio_context gives the English.

How a composed sentence is built

The core composes such a sentence with one helper, say(template, slots) (core/src/said.ts):

say("{who} walks to {place}", { who: { name: "Otto", gender: "m" }, place: say("the {thing}", { thing: "helm" }) })

It returns two things:

  • text: the English, the template filled. It is what the core always said, character for character: the command line, Claude and the logs read it, and every test that reads it is unchanged. (Proven over the staging corpus and the demo: the reading's English before and after say() is identical, 504 cases, 1,186 staged lines.)
  • said: the template and what fills each slot, for Studio. A slot holds a name the show gives (a character, a set, a prop: said as it is in every language; a character's carries their grammatical gender from their pronouns: she f, he m, anything else other), a number, or a piece that is itself a say(): "in a whisper", "excited", "the {thing}".

The English template is the sentence's key in Studio's catalog (context core), as it is for the core's whole messages: there is no second id to keep in step with the words. tools/i18n/extract.ts reads every say() in core/src into the template, each piece its own entry. A template whose only words are its slots ("{list}, {next}") is said as it is everywhere, and is left out.

How it reaches Studio

Beside each English string, session protocol 1.8 sends its said (all optional, so an older Studio reads what it always did): stagedSaid beside a reading item's staged, a reading scene's openingSaid, whenSaid and setNameSaid, an EpisodeAction's said, a PreviewRef's captionSaid, a Diagnostic's and a reading note's said, and chat.focus' suggestionsSaid beside its suggestions. A said is a list of parts, the sentence first: each a template and its slots, a piece naming the part that fills it by its index (the schemas allow no recursion, so the tree is flattened). A said whose first template is empty means the core had none: Studio says the English.

A finding can say two different things. Its message and hint are what Claude acts on: the ids to build from, the tool to call. Where the person would read those, the core sets said and hintSaid to the same finding in their words, what they see and do in Studio (forPerson in core/src/diagnostics.ts, the sentences in core/src/note-words.ts). A proposal's notes keep both (ChangeSet 1.3, session protocol 1.18: hintSaid); Review says the person's, a tool result sends Claude its own (withoutSaid).

How Studio translates it

RwLang.said(said, english) looks the template up in the catalog (context core) and fills each slot: a name as it is, a number in figures, a piece translated in turn. A piece sees the slots of the sentences around it, so a translation agrees with them. A translation, never the English, may choose its words with a small part of ICU's syntax:

  • {who.gender, select, f {cansada} m {cansado} other {con cansancio}}: by a slot's gender.
  • {count, plural, one {…} other {…}}: by a number.

Without a said (an older daemon), Studio finds the English among the core's messages as before (RwLang.core). In the pseudo-locale a template is accented and lengthened around its slots, which stay whole.

The director's notes, and what Claude reads

A note the director puts together from pieces (gesture-framing.ts: "Otto's wave played outside the frame of a close-up: the shot is widened to a medium shot to show it") is made with say() too, and diagnostic() keeps its said beside its message. A gesture's name is a piece (listed()): the extractor puts the library's gesture names in the catalog, and a show's own gesture is said as it is. A note whose message is whole, with only names and numbers put in it, is found by its English (RwLang.core) as before.

Claude reads the English. Where a tool's result leaves for Claude (outcomeText in core/src/mcp/server.ts, resultText in core/src/showrunner/agent.ts) and where the MCP server reads a resource out (readResource), withoutSaid() takes out every field that holds a said; a test on each proves none carries one. The command line's --json (reelwright preview, direct, asset) prints the English alone too. A change set keeps its notes' English alone (Review does not show them).

A use's places, as Studio lists them before a delete, are rebuilt by Studio (RwDocActions.place_words) from the English's shape and the use's pointers: an item of a list by the list's name as the form says it ("“The kettle hiccup” en Gags"), a field by its name ("Reparto"), "línea 12", "todo el documento". find_uses' data is Claude's too, and this needs nothing beside it.

Spanish

  • No article before a thing the show names: Spanish cannot know the gender of "eng console". "Otto mira hacia eng console", not "el eng console". A piece the catalog names keeps its article, translated with it ("the lore" is "la historia").
  • A character whose pronouns are neither she nor he is said with wording that needs no agreement ("con cara de emoción"), never "-e" endings.

Follow-up, not in this branch: an optional grammatical gender on a piece in Fields, so a Spanish show can say "la consola" of a thing it names.

A change's words, and a fix's

A change the core proposes has a title and a rationale, which Review, the chat's cards, the Show page, History and every editor that shows a proposal say. Each the core writes is a sentence of its own in core/src/studio/change-words.ts, whole for each case ("Create 1 prop: …", "Create {count} props: …"), with a kind of document a piece in the catalog once ("Create {kind} "{name}"", the kind "character"). The tools that propose a change take its title from there, so no file Claude reads has to put its own words through say().

The change set keeps the words beside the English: titleSaid and rationaleSaid (ChangeSet 1.1; a change set without them stays 1.0 and reads as before). Session protocol 1.13 carries them on to Studio: ChangeSummary.titleSaid, ChangesGetResult.rationaleSaid, a reading fix's labelSaid beside its label, a fix's tool's rationaleSaid beside its input, and a booth take's readingSaid beside its reading ("+1.2 semitones, livelier"). When Studio runs a fix it gives the rationale's words back with tools.call (ToolsCallParams.rationaleSaid); the change keeps them only while they say the same English as the rationale (callRationale() in core/src/studio/tools/registry.ts), so a rationale Claude wrote is never said with words it did not write.

Studio says a title through RwValueFormat.change_title, a rationale through RwValueFormat.change_rationale (a person's or the showrunner's own words stay as written), a fix's button through RwScriptReading.label_of, and a take's reading through RwLang.said.

The service's pages and mails

The web service says its own pages and mails in the person's language too: the sign-in page and its script's words, the waitlist, the refusals the sign-in page shows, the code's email and every other mail a person gets (a sign-in link, an invitation, a show's invitation, a notice, the digest), an invitation's and a stop link's page, the page that asks before Claude connects, and the error, maintenance and placeholder pages. Each sentence is put together with say() in core/src/host/pages.ts, mails.ts, collab-pages.ts and consent-pages.ts, so the catalog holds it as it holds Studio's words, and core/src/host/words.ts says it on the server as RwLang.said does in Studio, from engine/translations/<code>.po (the host image ships it): sayIn for text, htmlIn for a page or a mail (the words escaped, the slots it names in bold wherever the language puts them), coreIn for a message the core made whole (a refusal), found by its English. English is the sentence as written, never looked up, so an English page or mail reads exactly as it did. A page's script gets its words from the page (document.body.dataset.words).

Which language:

  • A page: the one the person chose in Studio's Settings, else the first their browser asks for (Accept-Language, by its q values; "es-MX" is es) that the service speaks, else English. <html lang> says which.
  • A mail to an account: its chosen language, else the one it last signed in with (the browser's, or Studio's own sent with the sign-in; never over a choice), else English.
  • The code's email: the account's choice, else the language of the page or Studio that asked.
  • A waitlist invitation: the one they joined in, kept on their row. An owner's invitation from the console: the same when they were waiting, else English.
  • A show's invitation: the invited account's, else the inviter's.

Studio's Settings › Appearance › Language on the web tells the service (POST /api/me/language; "" follows the browser again). users.language keeps it, with users.language_chosen saying whether the person chose it.

The web Studio's loading page (engine/app/web/shell.html) shows before Studio has started: its words are Studio's in the catalog, and tools/web/build-studio.sh writes each language's into the exported page (tools/i18n/shell-words.ts), which says them in the language chosen in Studio (kept in the browser under reelwright.language), else the browser's, else English.

Still in English: the operator's console and its test mail, what Claude reads (the MCP and OAuth answers), and the technical detail of why an app's published metadata was refused (under the translated words that it cannot connect).

What the catalog holds

The core's words a person can see in Studio, and only those: tools/i18n/extract.ts leaves out the command line's and the files whose words reach only Claude (the MCP and OAuth answers) or the service's operator (the console and its settings, the daemons, the render machines, the replay of recorded model calls), listed in NOT_STUDIO. --check fails when any message it holds, the core's too, is untranslated in a catalog. A message a word was put into ("ducks", "slower", "sit", " or ") is written whole for each case instead, and a list of ids reads "any of a, b".

Still in English: a Studio-side rationale (the script editor's "Written in Studio's script editor."), the timeline's shot problems and their mends, and a few director's notes with a side put in ("looks screen-left").

Keeping it so

The pseudo-locale's tour (tools/ux-tour.sh --language pseudo --check) fails on any word on screen not said through RwLang and on any of the core's words shown in English; CI runs it with --showrunner, so the chat and its suggestion chips are in it.

tools/check-plain-words.ts fails on a template literal that puts words around what it puts in, in any core file that composes with say(), unless it is a message (found whole by the catalog) or its line is marked plain-words-ok (an id, a key). Those written before the check are in tools/said-known.txt, and pass until edited. A say() whose template is not written in the call fails too: Studio could not find it.