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 asay(): "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.