Interface languages
How the interface’s words are held, chosen and checked (Blueprint §29, ADR 46). English (en) is the source.
Simplified Chinese (zh-Hans, 简体中文) is offered beside it since 2 October 2026, when it left preview (The preview
flag, below): a browser that asks for Simplified Chinese gets it, Settings > Language lists it, and the pages before
sign-in carry a switch between the two.
What stays English whatever the viewer chooses, permanently: everything a program reads. The API’s bodies and
error messages, mailda and its --json, doctor’s text, MCP, the Agent Skill, the SDK, the operational log,
audit keys and SMTP replies are byte-stable for the agents and gates that parse them (mailda deploy reads
doctor’s verdict from its first line). The interface shows the Node’s own English inside <NodeWords>, marked
lang="en". The CLI’s interactive prose is deferred rather than forbidden: it is translated when an operator
asks for it, chosen by MAILDA_LANG, then LC_ALL, then LANG.
The sender’s content is never translated, and neither the message frame nor a plain-text body takes the
interface’s language. Each keeps the message’s own: the sanitiser keeps a sender’s lang and dir on every
element it keeps (bdo and bdi among them), and the frame’s root carries data-script (sc, tc, jp: which
glyph forms Han is drawn in, never lang) from what the message says, in order the rendered body’s charset (GB2312,
GBK and GB18030 are sc, Big5 tc, ISO-2022-JP, Shift_JIS and EUC-JP jp; never an attachment’s), then its
Content-Language, then the HTML’s own root lang, and only then the reader’s locale (zh-Hans guesses sc,
English guesses nothing). A sender’s lang on an inner element picks that element’s forms over the frame’s
([lang]:lang(ja) and its siblings in frame.css). A plain-text body is drawn in the shell’s document, so its
<pre> carries lang="" and the same data-script, with the frame’s families (scriptFamily in
apps/node/worker/src/theme.ts). The body route returns the message’s answer as script
(apps/node/worker/src/render/script.ts), a string on the wire that the reader narrows to BODY_SCRIPTS. A UTF-8
message that says none of these says nothing, and an English reader then gets the platform’s own choice.
Where the words live
Section titled “Where the words live”apps/node/worker/src/i18n/ locales.ts LOCALES: tag, direction, endonym, preview. OFFERED: the non-preview ones (offeredOf). format.ts the runtime, pure: Message, Plural, Params, Text, parts(), text(), negotiate(), resolveLocale() areas.ts AREAS: each catalog area and the key prefixes it owns en/<area>.ts the source words, one module per area; their literals are the type zh-Hans/<area>.ts each area's twin, typed Twin<typeof en area> en/index.ts preauth, and app = every other area joined (and zh-Hans/index.ts) catalog.ts Catalog, Key, Source, Twin, CATALOGS preauth.ts every locale's preauth table, and nothing else (what /app/locale.js bundles) served.ts each locale's app table as a module, and its content-tagged URL glossary.ts CONCEPTS, NEVER, NEGATESapps/node/worker/src/client/locale.ts the browser half: bootLocale, install, loadApp, t, richapps/node/worker/src/client/app/format.ts every date, time and number the interface formatsapps/node/worker/src/client/app/words.tsx <NodeWords>, marked(), sentence()apps/node/worker/src/client/app/title.ts the per-route document titleapps/node/worker/src/client/app/ui/ime.ts isComposingKeyapps/node/worker/src/client/app/delivery-words.ts the words for the tokens /app/delivery.js returnsHow the words reach a page. scripts/build-client.mjs builds src/client/locale.ts into /app/locale.js,
a second entry point beside the shell, with every locale’s preauth table in it. The framework-free script
imports it first and calls bootLocale(), which picks the locale and sets <html lang>, dir, the title and the
wordmark before anything renders. When somebody signs in, handOverToShell() preloads the shell and fetches the
locale’s app table from /app/messages/<locale>.<tag>.js inside the same try, so a table that cannot be
fetched shows the “could not be loaded” notice. The tag is computed from the table’s text by the build and by the
Worker from the same catalogs, so the URL is immutable. An old tag of a known locale, which a sign-in tab left
open across a deploy still names, is a no-store 307 to the current table: the old address is never handed new
words, and the sign-in does not fail on a 404. The shell leaves
/app/locale.js external, so one module instance holds the one table.
What is migrated
Section titled “What is migrated”UNMIGRATED in apps/node/worker/test/node/untranslated.registry.ts is the list of what is not; a file leaves it
when its count reaches zero.
- Layer 1 (30 September 2026): the navigation and the rail, the command palette, the health popover and notices, the Inbox’s message list, Settings with its Language section, every screen’s title, and the interface’s own failure sentences.
- Layer 2a (1 October 2026): the reader (
reader.tsx), the composer and Drafts (composer.tsx,drafts.tsx), the Queue (queue.tsx), and the ledgers (ledgers.tsx: Outbox, Audit, Log, Doctor), with the sending vocabulary.delivery.client.jskeeps its decisions (which state, reason and summary a reader is shown, worst first) and now returns the contract’s tokens; the words are the catalog’ssend.state.*,send.reason.*,delivery.state.*anddelivery.reason.*, each with a.note. Its areas arereader,composer,drafts,queue,ledgersanddelivery. - Layer 2b (1 October 2026): every other React screen. Setup and its progress (
setup.tsx,onboarding.tsx), People, Agents and Approvals (people.tsx,agents.tsx,approvals.tsx), Matters and Limits (matters.tsx,limits.tsx), Butlers and Rules (butlers.tsx,policies.tsx), the first-run screen and the next steps (first-run.tsx,next-steps.tsx), and the menu, popover and section tabs (ui/), which had no words, only selectors. Its areas aresetup,onboarding,people,agents,approvals,matters,limits,butlers,policiesandui. - Layer 3 (2 October 2026): the framework-free scripts before sign-in,
app.client.js,session.client.jsandtheme.client.js(the claim and its recovery codes, sign-in with a password or a passkey, the invitation, the session’s endings and its timer in the status strip, the theme), soUNMIGRATEDis empty. Their words are thepreautharea, bundled into/app/locale.js. There is no React before sign-in, so the Node’s English is marked bynodeWords()inapp.client.js, the DOM twin of<NodeWords>: a<span lang="en">. A refusal before sign-in is keyed by the contract’s closed listPREAUTH_ERRORS(packages/contract/src/preauth-errors.ts, eighteen codes, no imports so/app/locale.jscan bundle it): a listed code shows a headline in the viewer’s language,preauth.refusal.<code>, with the Node’s English beside it, marked (D34); an unlisted code shows the Node’s English alone, marked, so a new refusal is never hidden or mistranslated. The headline is left out where the Node’s sentence begins with it, so English never says one sentence twice. The<noscript>notice insrc/ui.tsis one block per locale, English first, each with itslang. The status strip has a language switch (below, Choosing a language), and the sign-in and claim pages draw the lockup (below, Brand). The same layer keyed three things the Node sends with a closed code beside them: a Doctor finding’s check (doctor.check.*, keyed byDOCTOR_CHECKS, a title above the check’s name, which stays marked), a capability’s description on Agents (capability.*, keyed byCAPABILITY_IDS, English held equal to the contract’ssaysbyapps/node/worker/test/node/capability-words.test.ts), and a body’s problem (reader.body.problem.*, keyed byBODY_PROBLEMS, English held equal to the Node’sproblemSentencebyapps/node/worker/test/render.test.ts). A Doctor finding’sdetailandfixand the other sentences the Node writes without a code beside them stay the Node’s English, marked: one check writes several sentences, so keying them needs a code per sentence. A mint’s notice is the exception, said by the screen asagents.mint.tokenfrom the minted agent’sexpiresAt(10 October 2026), because the Node’s sentence carried the expiry as a raw ISO instant.
Every migration in layer 2a was held to its English by goldens written from the file before its first edit
(apps/node/worker/test/client/golden/), so a changed English word is a deliberate one, listed below the D table.
Layer 2b’s goldens were written the same way, on 1 October 2026 before any of its screens moved a word, and its
areas (onboarding, people, agents, approvals, matters, limits, butlers, policies, ui) were
registered empty first, so the screens migrate in parallel without two people editing one module.
Kept forwards (ADR 47, 3 October 2026) added words in two areas, each in en and zh-Hans: people.forward.* (the
line under an address that keeps a forward) and, in setup, setup.verified.listed, .showAddresses,
.addresses, .state.*, setup.destination.* and setup.rules.keptForward, plus the doctor title
doctor.check.kept_forwards. They use confirmed words where a glossary row exists: 已验证的目标地址 for verified
destination (delivery.verified_destination), and 转发 as the composer and the reader already say it. Three words had no row: kept forward (保留的转发), waiting for verification (等待验证) and register a destination
(登记). The owner accepted them in the working session on 3 October 2026 (round five, OWNER_ROUND_FIVE), and they are
bound to their keys. The
take-over’s own choice and its consequence (“receive here and keep forwarding to X”) are the Node’s words, shown
marked, as the other take-over choices are. A refusal Cloudflare gave a forward is its own English, marked.
Copies when a kept forward cannot be used (ADR 47 amended, 3 October 2026) added people.forward.copy.*,
setup.rules.copy, .copyAbout and .copyOn, and the Outbox’s ledgers.outbox.copy, .copy.note and .copy.noResend,
each in en and zh-Hans. They use confirmed words where a row exists:
定稿 for seal (never 封存), 隔离 for quarantine, 发件箱 for the Outbox, 管理员 for administrator, 转发 for the forward, and
the send’s own state from send.state.*. Copy (副本) is the new concept, a sealed send of a stored message that a refused kept forward asked for, never 同步
and not the e-discovery 外发; via in ”OWNER_ROUND_SIX); 副本 is a row bound to its keys. Why a copy was not sealed is the Node’s English, marked.
Forwards to several destinations (ADR 47 amended, 7 October 2026) added people.forward.edit* (the Change forwards
box), setup.rules.forwardTo, .foundIn, .foundNothing and .forwardsTo, each in en and zh-Hans, and changed three:
doctor.check.kept_forwards is Forwards (转发), since the check now counts destinations an administrator chose as well
as kept ones; people.forward.none says since it was added (添加之后), not since it was kept; and
setup.destination.about says a forward (转发). Forward (转发) is the new row, the concept a kept forward is one
case of; the owner accepted it in the working session on 7 October 2026 (round seven, OWNER_ROUND_SEVEN). Kept
forward (保留的转发) keeps its row, now bound to no key. The Worker rule’s choice (“receive here and forward to
addresses you choose”) is the Node’s words, marked, as the other take-over choices are.
People’s permission grid (7 October 2026) added people.relation.* (each permission’s short name, a column header),
people.grant.box, people.grant.legend and people.forward.copy.what, each in en and zh-Hans. They use confirmed
words where a row exists: 审批 for approval, 导出 for export, 管理员 for administrator, 邮件 and 发送 as the composer says
them. No new concept: each short name is a label for a relation whose id and full sentence (api.grant.*) stand
beside it in the legend.
Adding a string
Section titled “Adding a string”- Put the English in the area module of the file you are migrating (
en/inbox.tsforinbox.tsx), under that area’s prefix:"inbox.empty": "Nothing here yet.". A key outside the prefix does not compile. - Put its translation under the same key in
zh-Hans/inbox.ts. A missing or extra key does not compile. - Render it:
t("inbox.empty"), ort("inbox.count", { n })for a plural. A misspelt key or parameter does not compile. A sentence with an element inside it usessentence("key", { who: <strong>{name}</strong> }), so the translation moves the element and cannot add one. - Delete the literal, then lower the file’s count in
UNMIGRATED(apps/node/worker/test/node/untranslated.registry.ts) by what you removed. The test fails until the number matches, in either direction. - A literal the scan flags that is not words (an API path, a storage key, a media query) goes in
NOT_PROSE, under its file, with its reason.
Rules a check cannot see:
- A key is a concept, not an English phrase. A change of meaning renames the key, so the translation stops compiling and is redone. A wording tweak under the same key is caught only in review.
nis always the count, and the measure word is inside the message:{n} 封邮件, nevern + " 封". Two counts are two messages, each a plural on its ownn, filled into the sentence as parameters (ledgers.collect.done).50+stays50+, never 共 50 封 (a page is never a total). A noun passed into a shared template brings its own measure word, because the word depends on the noun:chrome.truncatedis仅显示最新的 {n} {noun}(a plural on the count shown), anddrafts.nounis 份草稿,chrome.notices.noun条通知 (F5, below).- Closed vocabularies are keyed by their tokens:
route.${AppRoute}must have a key for every route, by type. A wire token outside the union renders raw, in<code>(shown()indelivery-words.ts). The sending vocabulary is the model: the contract declaresSEND_STATES,SEND_REASONS,DELIVERY_STATESandDELIVERY_REASONSasas constlists while the wire staysz.string();en/delivery.tssatisfies Record<DeliveryKey, string>over them, so a token without words, or words for no token, does not compile;/app/delivery.jsdecides which token a reader is shown and returns it, anddelivery-words.tsnarrows it withoneOfbefore building the key (label atsend.state.<token>, note atsend.state.<token>.note). Closed vocabularies (round three, G12): the contract also declaresBUTLER_RUN_STATES,BUTLER_VERSION_STATES,BUTLER_PAUSE_REASONS,MATTER_TYPES,EXPORT_STATESandRATE_BREAKER_NAMES, and the Node’s modules that write them use those lists rather than copies of their own (apps/node/worker/src/butler/record.ts,apps/node/worker/src/butlers.ts,apps/node/worker/src/butler/pause.ts,apps/node/worker/src/matters.ts,apps/node/worker/src/exports.ts,apps/node/worker/src/breakers.ts). Five are keyed the same way (matters.type.*,matters.export.*,butlers.version.*,butlers.pauseReason.*,limits.breaker.*), English unchanged; a token outside the list renders raw in<NodeWords>.matters.type.legal_holdis bound to thelegal-holdrow. A Butler run’s state is keyed since round four (H7, below) asbutlers.run.*, in the runs table, the dry run’s verdict and the line naming the run a dry run reads (its trigger event in<code>): English the token with an underscore read as a space (“awaiting release”), zh 运行中, 等待放行, 已完成, 已终止, 被拒绝, 失败.stoppedis 已终止, never 已停止, because 停止 is thepauserow’s avoided word, and thebutler-run.stoppedrow holds it. Layer 3 addedPREAUTH_ERRORS,CAPABILITY_IDS,BODY_PROBLEMS,AUDIT_OUTCOMESandAPPROVAL_SUBJECT_KINDSto the contract’s lists, keyed the same way, and keyedDOCTOR_CHECKS, which the contract already had. The fifteen Chinese words of round three’s five lists (法律保全, 安全事件, 离职交接, 监管要求; 已申请, 运行中, 已完成, 已中止; 草稿, 已发布, 已被取代; 检测到循环; 发信量, 退信率, 投诉率) are keys, not glossary rows (only 法律保全 is bound to one,legal-hold, confirmed in the first review), and the owner accepted them as written in round four (below). - Never persist a
t()string as data (critic L10). A default a viewer’s interface writes into the Node (a mailbox name, a label) would become a word every other viewer reads in the writer’s language. One exception, by design: a reply’s quote line (composer.quote, “On {date}, {from} wrote:” / “{date},{from} 写道:”) is written into the mail in its author’s interface language, as Gmail and Outlook write theirs, because it is part of what the author wrote (ADR 46: persisted prose stays in its writer’s language). Its date isformat.zonedTime, which carries the offset from UTC (GMT+08:00), so a correspondent in another zone does not misread the hour. The subject prefixes are not the interface’s words:Re:andFwd:stay English on the wire in every locale, andreplySubject/forwardSubjectinsrc/client/app/screens/composer.tsxdo not add one after a Chinese client’s own (回复:,答复:,转发:, either colon). - The Node’s own English goes through
<NodeWords>: an errormessage, a doctordetail, a breaker’s sentence. Only the Node’s: a failure’s words may instead be this interface’s fallback (“this Node answered 503”, translated), soapi.tscarries who wrote them (Said:messageandfromNode, onReadFailureand on every refusal) and a screen shows a failure withmarked(failure), which wraps only the Node’s. A toast’stextand<Nothing detail>take a node for this reason; a sentence with the Node’s words inside it issentence()withmarked()as the argument, nevert()with the words as a string. Before sign-in, a refusal’s code is keyed byPREAUTH_ERRORS(layer 3); after it, per-code interface text for an error still waits for anERROR_CODESregistry in the contract. - A token is an identifier or a word, never bare (layer 3). A token the Node sends that names something a reader
needs in their language is keyed from a contract list (an approval’s subject,
APPROVAL_SUBJECT_KINDS; an audit entry’s outcome,AUDIT_OUTCOMES; a matter’s type). One that is an identifier (an audit action, a log event, a grant’s scope, an SPF, DKIM or DMARC result, a DMARC policy, a Butler’syamlorjson, a provider’s bounce type) is shown as itself in<code>, the Register’s rule, never bare beside Chinese. A token inside atitlecannot be marked, so it moves into visible text (the bounce type beside its state). A unit after a figure a screen rounds itself is a catalog word too (composer.size.kb,composer.size.mb). - Formatting goes through
src/client/app/format.ts. For an English viewer it formats exactly as before locales existed (the browser’s default locale). The one table is English’s month names (MONTHS, forshortTime,monthDayandrecordDay:15:09,26 Sep,26 Sep 2025), becauseIntl’s short month for September changed to “Sept” across ICU versions; another locale asksIntlfor its own numeric forms (15:09,9月26日,2025/9/26). Durations and counts are plurals keyed by the count (queue.duration.*), and nothing compares formatted text: the Queue’s “just now” is decided on the number of milliseconds, never on the words. - Two sentences side by side are joined with
t("join.sentence"), never a literal{" "}: a space in English, nothing in Chinese, whose 。 and ( carry their own spacing (G14). The gap after a control or a Latin token is not a sentence join and stays a space. - Every Enter handler asks
isComposingKey(event.nativeEvent)first. An IME’s Enter commits a candidate.apps/node/worker/test/node/ime.test.tsparses the client for every comparison of akeywith"Enter"and fails on one whose function does not ask.
Adding a locale
Section titled “Adding a locale”- Add it to
LOCALESwithpreview: true, its direction and its endonym. - Create
<tag>/<area>.tsfor every area and<tag>/index.ts; add it toCATALOGSandPREAUTH. Each is a compile error until it exists. - Add its column to every glossary row. Rows start
proposed. - Review it with
?locale=<tag>. - Take
previewoff whenUNMIGRATEDis empty and every row its keys use is confirmed (below).
The preview flag
Section titled “The preview flag”A preview locale is reachable only through ?locale=<tag> in the address: browser negotiation never picks
it, a stored choice naming it is ignored, and Settings > Language does not list it. The flag applies to that page
load, is never stored, and works where the browser refuses storage. Settings says, when it is in force, that the
page is in that language only because its address asks for it. So no viewer who did not ask is shown a screen that
is partly English, and the lang="en" wrapping of untranslated components that a shipped partial locale would
need is not built.
preview comes off when UNMIGRATED is empty, a pseudo-locale render of every route including its error states
finds no English outside lang="en" (T4, below), every string the Node supplies on the locale’s screens
renders through marked() or <NodeWords> (the reader, the composer, the Queue and the ledgers since layer 2a, every
React screen since layer 2b, and the pages before sign-in through nodeWords() since layer 3), and
the glossary rows the locale’s keys use are confirmed.
zh-Hans left preview on 2 October 2026. The owner asked for the interface’s languages to be completed and then merged “once things are ready”, with every glossary row confirmed in rounds one to four. The four conditions were checked by running them that day, before the flag changed:
| Condition | Evidence on 2 October 2026 |
|---|---|
UNMIGRATED is empty | UNMIGRATED is {} in apps/node/worker/test/node/untranslated.registry.ts; T3’s 18 tests green |
| T4 finds no bare English on any route or page before sign-in | pseudo-locale.test.tsx and pseudo-preauth.test.ts, 101 tests green: every route in APP_ROUTES populated, empty, every read failed, every button refused, and each refusal before sign-in |
| Every string the Node supplies is marked | T4 fails on the Node’s English outside lang="en", and the G goldens hold each screen’s marking (layer 2a, 2b and 3 rows of The checks); the whole client suite, 848 tests, green |
| Every glossary row the keys use is confirmed | 130 rows, 111 of them binding a key, none proposed; T2’s confirmedBeforeShipping finds nothing with zh-Hans released |
What changed with it: negotiation picks zh-Hans for a browser asking for zh, zh-CN, zh-SG or zh-MY (any tag
that maximizes to Hans); zh-TW, zh-HK and zh-MO maximize to Hant and continue down the viewer’s own list. A
stored zh-Hans is honoured. Settings > Language lists English and 简体中文. The switch before sign-in is drawn for
every viewer, so the English goldens of the pages before sign-in gained it (preauth-words.test.ts’s header; no
English word changed). ?locale= works as before, for one page load. The mechanism stays for the next locale, held
with a registry that has a preview in it: apps/node/worker/test/node/locale-preview.test.ts (the decision) and
apps/node/worker/test/client/locale-preview.test.tsx (Settings and the switch).
The page without scripting is server-rendered and cannot read ?locale=, so <noscript> carries one block per
locale, English first, each marked with its lang. The owner kept it bilingual in round four (H3, below), and with
zh-Hans offered there is no preview left for it to show unasked.
Choosing a language
Section titled “Choosing a language”Settings > Language lists the offered locales, each in its own language (endonym, data rather than
Intl.DisplayNames): English and 简体中文. Were English the only one, it would say so (the preview mechanism’s test). Choosing flushes an open draft, stores the choice
in this browser (mailda.locale) and reloads; a draft the Node will not save stops the reload. Where the browser
will not keep the choice, the page reloads with ?locale= instead and says it will not last. Boot order:
?locale=, the stored choice (offered locales only), navigator.languages negotiated on language and script over
the offered locales (zh-TW is Traditional and matches nothing), then English.
Before sign-in (layer 3), Settings is out of reach, so a reader whose browser asks for another language could not
choose one. The status strip carries a switch: the offered locales, each by its endonym, and the active one when the
review flag chose a preview, so a preview page can be switched back. It reloads with ?locale=, never storage, so it
works where storage is refused. With one offered locale a page shows no switch, since one choice is no choice; since
zh-Hans left preview it appears for everyone, as it was built to, with no code change.
Not built, and the trigger for each: a per-person language on the Node (users.locale) and an organisation
default, when the Node first writes a message to a person or somebody asks for the choice to follow them across
browsers. The first frame shows lang="en" until the module runs, as the theme does; a cookie mirrored into a
server-rendered lang is the upgrade.
The glossary
Section titled “The glossary”glossary.ts has one row per concept: the word each locale uses, the words it avoids, and the keys bound to
it. “held” is five concepts with five Chinese words; “Mailda” is one (prose: true), so any English that names
it must say 淼达 in Chinese. A key bound in keys must equal the term (a label, a route name); a key bound in
sentences must contain it (Released to the queue, 已放回队列), for a concept whose English word means other things
elsewhere and so cannot be prose. An avoid list applies only to its concept’s keys. NEVER is for phrases wrong in
every position (发送成功, 投递成功, 送达, 必达, 使命必达, 秒达, 您, 废纸篓); a key whose text honestly negates one declares it in
NEGATES, and a person reads that key. 送达 joined in layer 2b and the owner confirmed it in round three.
Every row is proposed until the owner confirms it. A confirmed row carries { confirmedBy, record }, where
record says where and when the owner confirmed it: a link to the owner’s own written confirmation (a pull request
review, an issue comment, or a decision the owner recorded on a review page), or, where there is none, what the owner
said and where (round four, below), and nobody writes one for somebody else. A locale that
is not a preview may not ship a key bound to a proposed row. The check cannot tell who typed the field, so the real
gate is the owner’s approval of the pull request.
The owner’s review of 30 September 2026 answered every row and every reviewer flag on a review page, one
recorded decision each. 79 of the 81 rows were confirmed there (CONFIRMED in glossary.ts). Two words changed,
on three rows: Butler and Butlers are 管家 (the Latin Butler is now an avoided form in zh-Hans; the CLI noun
butler stays Latin in mono), and Trash is 回收站 (废纸篓 is in NEVER, so no sentence keeps it). Two rows were
held and answered on 1 October 2026 (OWNER_ANSWER). passkey stays 通行密钥: the owner first asked for 密钥,
which already names cryptographic keys here (密钥恢复副本 for escrow, the key vault) where Apple and Google say
通行密钥, was asked once, and chose 通行密钥. place.delete is 移到回收站, which the new Trash word made of the
confirmed 移到废纸篓, and which the owner then confirmed. Every row of that review is now confirmed.
Flags adopted with it: the rail’s count says {mine} 个由你处理; Health’s outbound area is “Outbound mail” in English (it covers handing over as well as delivery) and 出站邮件; the
org.admin grant names the holds that relation governs, 法律保全和隔离 (legal holds, and quarantine’s list and
release; releasing a rule’s hold on a send is send.propose, not org.admin); and 淼达 is confirmed as the mark.
The owner’s review, round two (1 October 2026), on the same review page, answered the three rows layer 2a added
and the reviewer’s ten flags on its 400 new strings, one recorded decision each, all confirming what was proposed.
The rows are confirmed with that round’s own record (ROUND_TWO in glossary.ts):
- send.denied, 否决, for “denied” in the send reasons (
send.reason.policy_denied,send.reason.approval_denied): a rule or an approver decided against a send. Avoids 拒绝, which is the refusal family of somebody else’s server. - recall, 撤回, in
composer.sendNoteandcomposer.how.body: taking a message back after it left, named only to say no Node can. Avoids 召回. It is the word the revoke (吊销), withdraw (收回), undo (撤销) and held rows all keep clear of. - vault, 密钥库, in
composer.bodyUnavailable.unreadable: the key generations that open this Node’s sealed content, restored with the recovery codes. Avoids 保险库 and 金库.
The owner’s review, round three, on the same review page, answered layer 2b’s twenty rows, 送达 in NEVER, and
the reviewer’s twenty flags on its 542 new strings (G1 to G20), one recorded decision each, all confirming what was
proposed (decisions stamped 1 October 2026, 15:50 to 15:52 UTC). The rows below carry that round’s own record
(ROUND_THREE in glossary.ts), so every row up to layer 2b is confirmed: apps/node/worker/test/node/catalog.test.ts
holds that each round’s rows carry that round’s record, that no row is proposed since round four (below), and that a
confirmed row put back to proposed is reported on every key it governs (breaker, recall, passkey, brand, and
round four’s evidence, claim-secret, butler-run.stopped).
Layer 2b’s rows, confirmed in round three. Twenty concepts its screens needed and no row had, each bound to the keys that use it:
| row | en | zh-Hans | avoids | what it names |
|---|---|---|---|---|
team | team | 团队 | a group an approval stage can require a decision from | |
mint | mint | 签发 | issuing a credential handed to someone (an invitation, an agent); re-minting 重新签发. Recovery codes are generated, 生成 (ledgers.codes.mint; G7) | |
capability | capability | 能力 | an agent’s ceiling unit; unbound (its plural defeats a binding), held by people-words.test.tsx | |
approve | Approve | 批准 | the decision, as approval (审批) is the request; Matters’ “two people have to approve” now says 批准 too | |
routing-rule | routing rule | 路由规则 | 策略 | Cloudflare Email Routing’s rule for an address, not this Node’s Rules (规则) |
pause | pause | 暂停 | stop, restart; 停止, 封禁 | D3’s one verb, for a domain and a Butler |
supervised-read | supervised read | 受监督查阅 | 监控, 监视 | a time-boxed grant to read a mailbox one holds nothing on |
export | export | 导出 | 外发 | an e-discovery copy that leaves this Node’s controls |
breaker | Breaker | 熔断器 | 断路器 | a rate this Node applies to itself (armed 已启用, unarmed 未启用, tripped 正在阻止发信) |
vouch | vouch | 作保 | 担保 | an administrator’s reasoned lift of a suppression |
lift | lift | 解除 | 释放 | ending a legal hold or a domain pause |
administrator | administrator | 管理员 | whoever holds org.admin | |
receipt.measured | receipt | 测量记录 | 回执, 接收记录 | the measurement behind a budget, not an arrival’s record (接收记录) |
onboard | onboard | 接入 | Cloudflare’s word for letting an account send as a domain; 为发信接入 where the English says for sending | |
zone | zone | 区域 | a Cloudflare zone | |
apex | apex | 根域名 | 顶级域名 | a zone’s own name; 顶级域名 is a top-level domain |
token | token | 令牌 | an API token | |
delivery-event | delivery event | 投递事件 | Cloudflare’s report of what a receiving server did with a send | |
subscription | subscription | 订阅 | the subscription that publishes those events into this Node’s queue | |
consumer | consumer | 消费者 | the Worker a queue hands its messages to |
Round three’s flags, each adopted as suggested (the first option where one was offered):
- Words. G1 was already decided (D30). G2: a deny rule’s outcome says “is denied by this rule” / 会被此规则否决,
bound to
send.denied(D32). G3: Setup’s delivery outcomes say what this Node does with Cloudflare’sdelivered, accepted (D31). G7: 签发 only for a credential handed to someone, 生成 for recovery codes; a live agent is 生效中 as a live Butler is; the consent wrangler holds is 已获得的授权 on the first-run screen as on Setup. G10: that consent stays 授权, and “Forget this token” 移除. G11: Cloudflare’s delivery-events queue is Cloudflare 队列, so 队列 alone is always the shared Queue. G13: the matter pickers’ first option stays 暂不关联事项. G15: an approval’s count is of people deciding, 批准, not of requests:{n} 人批准, and staged第 {stage} 阶段,共 {stages} 个阶段 · 共需 {n} 人批准(the count is the total, so 还需 does not apply; the English “Needs / 2 approvals needed” is unchanged). G17: the dry run’s button is 试运行:{at} 的那次运行. G19:limits.pauses.list“Domains paused now” / 当前暂停的域名, as shipped. - Dates. G4: Setup’s verified-destinations read time, Agents’ Expires and Limits’ suppression Since go through
format.ts’sdateTime, in the viewer’s zone (D33). - A failed read. G5: Limits’ paused domains show a failed read as failed (
<Nothing kind="failed">, the Node’s words marked), not as “No domain is paused.”, and show loading while the read is pending. Held by the refused-read golden and its assertion inapps/node/worker/test/client/matters-words.test.tsx. - The Node’s tokens. G8: where a routing rule sends mail, a routing status, the records already there and a routing
outcome’s rule are passed into their sentences as elements in
<NodeWords>(sentence()), and the relation and capability tokens on People and Agents are markedlang="en"inside their mono spans. English is byte-identical. - Joins. G14: a screen that sets two sentences side by side joins them with
join.sentence, a space in English and nothing in Chinese (below, Adding a string). Used at Setup’s outcome and rule seams, the progress line’s optional step, the first-run screen’s note and a gone mailbox’s aside. Three listed seams keep their space, because what comes before is not a sentence: the invitation’s mailbox box (a checkbox), an unpublished draft’s note (after the Publish button) and an agent’s partial ceiling (a Latin capability id, then3/4), where the Register’s Han-Latin space applies. - Layout. G16: a table cell under Chinese is at least four Han characters wide (Register, below).
- Bindings. G20: the butler row binds
butlers.notAdmin,butlers.runs.noneandbutlers.dry.notRun, and the rules row bindspolicies.notAdminandpolicies.empty; a bound sentence’s English now names its concept whatever the case (“No rules here” names Rules).butlers.runs.explainnames no Butler, andpolicies.captionandpolicies.newsay “rule”, another form of the row’s word, so the Rules screen’s rendered zh test holds them to 规则, never 策略. - Kept. G9: a new Butler’s and a new rule’s persisted defaults stay English (L10, data). G18: nothing to decide.
- G12: the closed vocabularies a Butler, a matter, an export and a breaker render are contract lists, five of them keyed (Adding a string, Closed vocabularies).
Round four, accepted without review (2 October 2026). The owner did not review layer 3’s rows or its questions
on a review page. In the working session on 2 October 2026 the owner said “just accept them”, and so accepted every
proposed row and every question (H1 to H14) as proposed. That is what OWNER_ROUND_FOUR in glossary.ts records,
with no link, because there is no page to link: its record says the owner accepted every proposed row and question as
proposed in the working session on 2 October 2026, without a review page. The pull request carrying it is the only
written trace, so the owner’s approval of it is the gate, as for every round. catalog.test.ts holds that no row is
proposed and that each round’s rows carry that round’s record.
Layer 3’s rows, accepted in round four. Twenty-five concepts the pages before sign-in, the Doctor’s check
titles, a capability’s description, a body’s problem and an audit entry’s outcome needed and no row had, each bound to
the keys that use it, and one more for H7, butler-run.stopped, which holds the word the owner took for a run that a
stop node ended (已终止, never 已停止). The claim-secret row now binds the claim’s field, preauth.claim.secret
(H1). Layer 3 also bound its keys to confirmed rows where the English
agrees: session, passkey, claim, recovery-codes and invitation-secret before sign-in, consumer and
delivery-event on two Doctor titles, and legal-hold, lift, supervised-read, export and pause in the
notices band’s approval subjects.
| row | en | zh-Hans | avoids | what it names |
|---|---|---|---|---|
node.listening | listening | 接收中 | a claimed Node’s state in the strip before sign-in | |
organization | Organization | 组织 | what the claim creates | |
owner | owner | 所有者 | the person who claims the Node | |
operator | operator | 运维人员 | whoever reads this Node’s log | |
renew | renew | 续期 | a session’s token renewal | |
passkey-sign-in | passkey sign-in | 通行密钥登录 | one passkey ceremony, which can expire or be spent | |
doctor.check | check | 检查项 | a Doctor finding’s subject | |
credential-key | credential key | 凭据密钥 | ||
signing-key | signing key | 签名密钥 | ||
key-generation | key generation | 密钥代 | 版本 | one generation of the keys that seal evidence |
evidence | evidence | 证据 | the stored bytes a send or an arrival is proven by | |
evidence-bucket | evidence bucket | 证据存储桶 | ||
catalog-db | Catalog database | 目录数据库 | the D1 database bound as CATALOG | |
migration | migration | 迁移 | ||
backlog | backlog | 积压 | ||
stranded | stranded | 滞留 | ||
orphaned | orphaned | 孤立 | ||
supervision-notice | supervision notice | 受监督查阅通知 | the notice §7 owes the person whose mail was read | |
transport | sending transport | 发信通道 | 适配器 | what hands a send to the mail service |
reduced-report | reduced report | 精简报告 | ||
sanitise | sanitising | 安全清理 | 净化, 消毒 | removing what is unsafe from a message’s HTML |
classifier | classifier | 分类器 | ||
triage | triage | 分拣 | ||
audit.refused | refused | 已拒绝 | 拒收, 否决 | an audit entry’s outcome: this Node refused the act |
workers-paid | Workers Paid | Workers Paid 套餐 | 付费版 | Cloudflare’s plan |
butler-run.stopped | stopped | 已终止 | 停止 | a Butler run a stop node ended, or whose release gate timed out (H7) |
Layer 3’s questions, as round four answered them (each with its keys in the review data), each as the working session decided:
- H1 (D35): the claim’s field says “Claim secret” in English, as the CLI
mailda claim-secretand the confirmed row do; zh was already 认领码. The Node’s ownbad_secretandnot_installedmessages said “bootstrap secret” until an accepted follow-up of their own (below). - H2: the Doctor’s check titles stay, in both languages. H3: the
<noscript>notice stays bilingual. - H4, H9, H10, H11: confirmed as written (
preauth.codes.lede,preauth.signin.title; D34’s headlines and “This Node refused a request that did not come from its own page.”; the notices band’s five approval subjects; 成功, 已拒绝 and 失败 for an audit outcome). - H5: kept as written: 证据密钥代, 发件箱出队, 与静默解释相矛盾的证据, 分拣, and “Delivery outcomes” (投递结果).
- H7: a Butler run’s state is keyed (
butlers.run.*; Closed vocabularies, above).
Accepted follow-ups, each its own change (the owner accepted them in round four, and said “go close them all”;
each was made in a commit of its own on 3 October 2026, docs/history.md “The round-four follow-ups”):
- The Node’s bootstrap messages (H1):
bad_secretandnot_installedsaid “bootstrap secret” where the field now says claim secret. Done: both say claim secret, the codes unchanged, with a note for the agents, as G6 has. The two headlines are bound to theclaim-secretrow, andapps/node/worker/test/claim.test.tsholds the Node’s sentence to the row’s word and to the headline, word for word. - H6: the notices band’s supervised headline, “was granted a supervised content of Support”, reworded in English
around the Node’s scope token (“a supervised read (content) of Support”). Done, and a grant with no recorded
scope has a sentence of its own (
chrome.notice.supervised.unscoped), so neither locale prints “read (read)” or (范围:查阅). Both are bound to thesupervised-readrow; held bychrome-words.test.tsxandchrome-zh.test.tsx. - H8 (D36): the Node’s
locked_outsentence gets a real plural for its minutes, with a note for the agents. Done: “Try again in 1 minute.” and ”… in 5 minutes.” (lockedOutMessageinapps/node/worker/src/routes/support.ts), held at one and at many, and through the route, byapps/node/worker/test/auth.test.ts. - H12: a text-only body with a problem shows its problem sentence, and a message with no body says so (§5C),
where today one shows no notice and the other an empty
<pre>. Done: above the plain text,reader.body.fallback.*, the Node’s sentence for that case (“Its plain-text alternative is shown instead.”, held equal toproblemSentencebyapps/node/worker/test/render.test.ts); in place of the empty<pre>,reader.body.none(“This message has no body.” / 此邮件没有正文。). No row was added: the Chinese uses words already shipped (纯文本版本 aspreauth.noscript.doctorhas it, 正文 asreader.body.frame), and both are listed for the owner below. - H13: People, Rules, Butlers and Matters say “not an administrator” on a 404 only, and show any other failed
read as failed, as G5 did for Limits. Done, by what the Node answers (verified in its routes):
GET /api/people,/api/policies,/api/butlersand/api/holdsanswer a non-administrator 404, so their sentence shows on a 404 only (answeredNotFoundinapps/node/worker/src/client/app/api.ts);GET /api/mattersnever refuses one (it lists their own matters), so every failure there is a failed read, andmatters.refusedis gone from both catalogs. Matters’ two lists show loading while pending. No other screen had the pattern. Held by the not-admin and failed-read cases inpeople-words.test.tsx,butlers-words.test.tsxandmatters-words.test.tsx. - H14: the backticked CLI commands inside catalog sentences (
mailda claim-secret,mailda deploy,mailda recovery-codes confirm) render in<code>, an English markup change. Done: the page before sign-in draws a catalog sentence’s backticked span as<code>, backticks dropped, in every locale: the claim’s secret hint, the recovery codes’ next step, and thebad_secretandnot_installedheadlines. The text is unchanged in both catalogs. The Node’s own English beside a headline is shown as it was sent. Held by the pre-sign-in goldens andpreauth-zh.test.ts.
For the owner, from the follow-ups (no row proposed, since zh-Hans is released): H12’s two new sentences use 纯文本版本 for a message’s plain-text alternative (改为显示它的纯文本版本。) and 正文 for its body (此邮件没有正文。). Neither is a glossary row. Both words already ship elsewhere, but the owner has not seen them used for these concepts. If either should be a row, it is a round of its own.
Not decided in round four: ending the preview. zh-Hans then met every condition of The preview flag (above), but
nobody had asked for preview to come off. The owner asked later the same day, and a change of its own ended it (The
preview flag, above).
G6, done in a change of its own (2 October 2026). The Node’s own English for a domain pause said stop and
restart where every screen says pause and lift. It now says pause and lift too: the refusals
(apps/node/worker/src/domain-pause.ts), the withheld send’s sentence (apps/node/worker/src/outbound/recheck.ts),
doctor’s domain_paused finding, the audit vocabulary’s two domain.pause_* entries, and the three
/api/domain-pauses summaries in the contract, which the SDK, the Agent Skill and MCP repeat, with the Skill’s reason
for withholding them from agents. Every code and field
name is unchanged; only prose moved, so an agent that matches a code reads the same, and one that matched the old
words has the note in docs/history.md. The summaries had two errors beside the verb: they said sending to a
domain, where a pause holds mail from one, and that lifting “takes more than one person”, where one administrator
lifts alone. apps/node/worker/test/domain-pause.test.ts holds this prose to the pause row’s avoided words. A
tripped breaker’s “stopping mail” is a breaker’s state, not a pause, and stays. A Butler’s pause was its own
follow-up, done on 3 October 2026 (below).
A Butler’s pause in the Node’s own English, done in a change of its own (3 October 2026). G6’s follow-up for a
Butler: the Node said a machine “stopped” a Butler and an administrator “restarted” it, where every screen says paused
and resume (D26). It now says pause and resume: the two /api/butler-pauses summaries in the contract (and so the
SDK’s comments), the audit vocabulary’s butler.paused and butler.resumed, the resume’s refusals
(apps/node/worker/src/butler/pause-acts.ts, where a resume also no longer “releases”, a held send’s and a gate’s
verb), doctor’s unreadable butler_paused finding and the Butler capability’s sentence on the loop. Every code and
field name is unchanged. apps/node/worker/test/butler-pause.test.ts holds this prose to the pause row’s avoided
words. Not a pause, and kept: a revoked relation “stops it on the next node”, a run a stop node ended is stopped
(butler-run.stopped, 已终止), and doctor’s silence check tells “a Butler that stopped running” from one nothing has
triggered.
Flags confirmed as they ship, no change: F1 the composer’s dock head says 正在回复:/ 正在转发:, so a Chinese
subject’s own 回复: is not read as a doubled prefix; F2 放行 for the three gates (a rule’s hold, a Butler’s gate,
a quarantined delivery), 放回队列 only for a case given back; F3 “held” as a case state is 他人处理中 and a held
delivery is 隔离; F4 queue.held.reason.dmarc_fail_reject says 要求收件方拒收, the receiving server’s verb;
F6 the composer’s “your node” stays 你的节点, the English’s own framing, not 本节点; F7 “policy” in the send
notes is 规则, and since D1 (layer 2b) the English says rule too, and “the mail service” is 邮件服务商; F8 DMARC’s own policy
stays 策略 (reader.auth.noPolicy); F10 a zh-Hans reader still guesses sc for a message that names no script,
so an English message’s Latin is drawn with the SC face’s. Two flags changed words: F5, the measure word moved
into each noun passed to chrome.truncated (drafts.noun 份草稿, chrome.notices.noun 条通知,
ledgers.noun.entries 条记录, ledgers.outbox.noun 条发件记录, queue.held.noun 封本节点上的隔离来信,
limits.suppressed.noun 个邮件地址), and F9, the
English plurals and the held notes (D10 to D13 below).
Register (zh-Hans)
Section titled “Register (zh-Hans)”- 你, not 您; 本节点, not 我们. The English speaks as the Node, not as a vendor.
- Full-width punctuation in sentences (,。:;?!) and “ ” quotes. Checked.
- One space between Han and a Latin letter or digit:
3 封邮件,Worker 与密钥. Checked in the catalog (T2), and made by the runtime where a filled value meets a Han template ({name}已发布withlead-responseislead-response 已发布), which no catalog check can see; never inside the value. - Identifiers stay Latin and in mono: relation tokens,
E_codes, budget names, CLI verbs, header names,.eml. - Han is not broken mid-word where a column is narrow: under
:root:lang(zh),thead th(layer 2a) and the buttons and links in a table cell (td button, td a, layer 2b) keep words whole (word-break: keep-all), since a phone width otherwise stacked 发布 and 恢复发信 one character per line. Text cells still wrap per character, so every table cell is at least4emwide under:root:lang(zh)(G16, round three): four Han characters, a presentation constant, intended so that 已启用 or 生效中 · v1 holds one line and a sentence wraps inside its column rather than down one character’s width.
The brand is 淼达 in the zh-Hans interface: the shell’s wordmark and the title ({screen} · 淼达). English
shows Mailda only. The sign-in and claim pages draw the lockup, 淼达 with a secondary <span lang="en">Mailda</span>
after it, quieter (bootLocale(), built in layer 3; .wordmark > span[lang] in src/shell-css.ts). mailda stays in every
identifier: the CLI, @mailda/*, mailda.site, MAILDA_*, headers, error codes. 淼达 is a homophone of 秒达,
“arrives in seconds”, a delivery slogan this product must never make; 秒达 is in NEVER. src/brand.ts records it.
English defects the translation must not copy
Section titled “English defects the translation must not copy”Fixed in English in the layer that migrates the file, before it is translated, or in the owner’s review that follows it (D10 to D13). Layer 2b fixed D1, D3 and D9 and found D14 to D28 while migrating, its verification D29, and the owner’s decisions on it D30 to D33:
| # | English | Fix |
|---|---|---|
| D1 | Rules (screen) vs “policy” in the send-reason notes, and Approvals’ “A policy asked for a second pair of eyes” | “A rule holds this send”, “A rule asked …” (fixed, layer 2b: the notes and Approvals; the labels keep “policy hold”, the glossary’s confirmed English, and “policy denied”, “policy is stricter now”, for the owner) |
| D2 | Automate / Automations / Butlers: one stem three times | zh keeps three words (自动化 / 自动化流程 / 管家) |
| D3 | Stopped vs paused domains; “three administrators” vs “two” | one verb (pause), and every sentence counts as apps/node/worker/src/domain-pause.ts does: two administrators besides whoever asks (“Pausing a domain takes the agreement of two other administrators; whoever asks is never one of them”). An export’s approvers hold approval.decide on the mailbox (apps/node/worker/src/exports.ts), so it says “Two other people”, not administrators. Limits’ “Stopped domains”, “Ask to stop”, “Restart” and Approvals’ “Stopping a domain’s mail” say pause and lift; the domain_paused note says “paused every send … can lift the pause”, not “stopped … restart the domain” (fixed, layer 2b) |
| D4 | Bootstrap secret vs install secret vs claim-secret | one word (认领码) |
| D5 | Outbox empty state “Nothing has been sent” (ADR 39) | “The Outbox is empty.” (fixed, layer 2a) |
| D8 | handed_over’s note says “Accepted by the mail service”, the delivery word | “Taken by the mail service.” (fixed, layer 2a) |
| D9 | Matters’ empty state “Nothing is under investigation”, and its lead “An investigation, and what it authorises” | “No matters you can see. An administrator sees every matter; anybody else sees the ones they opened.”, “A matter, and what it authorises.” (事项; fixed, layer 2b). It first said “No matters have been opened.”, which a member who is not an administrator reads as a fact about the organization; the Node filters their list rather than refusing it (3 October 2026) |
| D10 | “1 sends” (ledgers.outbox.count) | a plural on n: “1 send”, “6 sends” (fixed, round two) |
| D11 | “1 message(s) moved” (queue.merged); “within 1 minutes” (queue.target.set, queue.target.promised) | plurals on n (fixed, round two) |
| D12 | “(s)” in the Doctor’s remedies: ledgers.collect.done, ledgers.search.requeue, ledgers.evidence.* | one plural per count (ledgers.collect.orphans, .drafts, .exports; ledgers.evidence.checked, .checkedIn, .faultCount) filled into the sentence; “Requeue 1 message” (fixed, round two) |
| D13 | The held, awaiting and withheld notes say “Not sent (yet).” | this scale’s word: “Not handed over yet.” (held, awaiting), “Not handed over:” (withheld, which is final); 尚未移交, 未移交 (fixed, round two) |
| D14 | “1 record(s) still required” (Setup’s progress, routed and sending) | a plural on n (onboarding.required) (fixed, layer 2b) |
| D15 | “example.com now has 2 confirmed record(s)” (Setup, receiving) | a plural on n (setup.receiving.done.routed, .notRouted) (fixed, layer 2b) |
| D16 | “1 of the 2 address(es) this Node has handed mail to” (Setup, verified destinations) | two plurals: the sentence on the verified count (is/are), setup.verified.recipients on the other (fixed, layer 2b) |
| D17 | “Would publish 1 event types” (Setup, delivery outcomes) | a plural on n (setup.outcomes.publish) (fixed, layer 2b) |
| D18 | “next: mail routed to this node.” (a label lowercased, so Node became node) | each step has a .phrase, its in-sentence form: “next: mail routed to this Node.” It is also what the first-run screen’s “Next: …” and the shell’s notice say (fixed, layer 2b) |
| D19 | “This agent will hold 1 capability(s) across 1 mailbox relation(s)” | one plural per count (agents.review.capabilities, .relations) (fixed, layer 2b) |
| D20 | “2 pinned route(s) this Node no longer names” | a plural on n (agents.unnamed) (fixed, layer 2b) |
| D21 | “Showing the newest 1 drafts” (chrome.truncated) | a plural on the count shown: one is “Showing the newest one.” (fixed, layer 2b) |
| D22 | “message(s)” in chrome.notice.queries and chrome.notice.raw | plurals, the second count its own (chrome.notice.listed) (fixed, layer 2b) |
| D23 | Limits’ window “1 minutes” | limits.window.days, .hours, .minutes, each a plural (fixed, layer 2b) |
| D24 | Matters’ export object “1 bytes” | a plural on n (matters.exports.bytes), so the count is grouped: 1,024 bytes (fixed, layer 2b) |
| D25 | The dry run’s “4 node(s)” | a plural on n (butlers.dry.nodes) (fixed, layer 2b) |
| D26 | A paused Butler’s standing “stopped — loop detected”, beside a Resume button | “paused — loop detected” (D3’s verb; held by the pause row, which avoids stop) (fixed, layer 2b) |
| D27 | A rule’s volume clause “once this Node has sent 500 today”, whose counter is the hand-overs (apps/node/worker/src/policy.ts) | “has handed over 500 today” (ADR 39; held by the send.handed_over row) (fixed, layer 2b) |
| D28 | Setup’s record day “24 Sept 2026”, the browser’s ICU month | “24 Sep 2026”, from MONTHS as every other English date (recordDay) (fixed, layer 2b) |
| D29 | Limits’ paused-domains table carried its section’s name, which axe reported as a duplicate | its own name, limits.pauses.list, “Domains paused now” (fixed, layer 2b verification) |
| D30 | The send-reason labels “policy hold”, “policy denied” and “policy is stricter now”, where the notes say rule (D1) | “rule hold”, “rule denied”, “rules are stricter now”; the policy-hold row’s English with them (the owner’s decision, 1 October 2026) |
| D31 | Setup’s delivery outcomes list Cloudflare’s event kinds “delivered, bounced, complained”, while this Node reads delivered as accepted and the Chinese said 已受理 | “accepted (Cloudflare’s delivered), bounced, complained”, the token in mono; zh 已受理(Cloudflare 的 delivered)、退信或投诉 (round three, G3) |
| D32 | A deny rule’s outcome “is refused”, the provider’s word (send.refused), for this Node’s own rule deciding against a send | “is denied by this rule” / 会被此规则否决, bound to send.denied (round three, G2) |
| D33 | Setup’s verified-destinations read time and Agents’ Expires printed the wire’s ISO stamp, and Limits’ suppression Since sliced it to UTC with no zone | each is format.ts’s dateTime, in the viewer’s zone and locale (round three, G4) |
| D34 | A refusal before sign-in showed the Node’s four-part English alone (E_PASSKEY_REJECTED that passkey did not verify …): a code first, then prose for an operator | a headline in the viewer’s language above it, keyed by PREAUTH_ERRORS, the Node’s English kept beside it and marked; English gains the headline (fixed, layer 3) |
| D35 | The claim’s field says “Bootstrap secret”, the CLI mailda claim-secret, the glossary row “claim secret” (D4) | “Claim secret”, bound to the claim-secret row; zh was already 认领码 (fixed, round four, H1). The Node’s own two refusals say claim secret since their follow-up (3 October 2026) |
| D36 | locked_out’s “Try again in 5 minute(s).” | the headline carries no count, so the plural was not copied into the catalog; the Node’s own sentence has a real plural since H8’s follow-up (“5 minutes”, “1 minute”; 3 October 2026), the code and retry-after unchanged |
| D37 | The notices band printed an approval’s subject and a matter’s type as raw tokens (send_manifest, legal_hold) and its instants as raw ISO stamps | the subject in words (chrome.notice.kind.*, keyed by APPROVAL_SUBJECT_KINDS), the type in matters.type.*, each instant format.ts’s dateTime; the grant’s scope stays the Node’s token, in <code> (fixed, layer 3) |
Approvals now shows a domain pause’s domain and reason, in both locales: the card read pause while the Node
sends domainPause, and the test fixture had the screen’s shape rather than the Node’s, so it never showed them
(found on screen in layer 2b’s verification; the fixture now has the Node’s shape).
D5, D8 and D13 are held by apps/node/worker/test/node/delivery-summary.test.ts (“the Outbox’s words keep to their
scale”): no send.state.* word says accepted, bounced or delivered (受理, 退信, 送达), only never_submitted’s note says
sent, and only to say nothing was, the held, awaiting and withheld notes say not handed over, and the empty Outbox does
not say sent. D10 to D12 are held by the English tests of the screens that show them, at one and at many
(apps/node/worker/test/client/ledgers-words.test.tsx, queue-words.test.tsx, doctor-actions.test.tsx). The
evidence verdict’s {bytes} bytes read keeps its ungrouped string count and is not a plural, so a batch of exactly
one byte would read “1 bytes read”; a plural there would group the count the goldens pin as 1024.
D14 to D17 are held by apps/node/worker/test/client/setup-words.test.tsx and onboarding.test.ts at one and at many;
D19 and D20 by people-words.test.tsx; D21 and D22 by chrome-words.test.tsx, drafts.test.tsx and
truncated.test.tsx; D23 and D24 by the Limits and Matters goldens (matters-words.test.tsx); D25 by
butlers-words.test.tsx; D28 by onboarding.test.ts. D3’s pause, D26 and D27 are bound in the glossary, so the
English going back fails T2; D1’s notes are held by butlers-words.test.tsx (“calls a rule a rule in every send note”).
D31 is held by the Setup golden and its zh test in setup-words.test.tsx; D34 by the pre-sign-in goldens and
apps/node/worker/test/client/preauth-zh.test.ts; D37 by chrome-words.test.tsx; D32 by the Rules goldens and the
send.denied row; D33 by the Setup, Agents and Limits goldens, where each instant is the viewer’s toLocaleString(), and
by setup-screen.test.tsx.
Other English changed on purpose in layer 2a, none of it a D row:
- The reader’s thread row named a send’s state by its raw token (
handed_over); it now says the catalog’s label (“handed over”). - A reply’s quote line carries the offset from UTC: “On 8/21/2026, 17:00:00 GMT+08:00, alice@… wrote:” (critic L5).
- Numbers are grouped: an attachment of 1234 KB reads 1,234 KB, and a Queue deadline in 26438 days reads 26,438d. A count whose English was pinned ungrouped (“1024 bytes read”) is passed as a string and keeps its form.
- The Outbox’s Retry no longer trails a
·when no.emllink follows it (a send never submitted has none).
And in layer 3 (the pre-sign-in goldens, preauth.*.en.html under apps/node/worker/test/client/golden/, were
written from the scripts before their first edit):
- A refusal before sign-in gains a headline above the Node’s sentence where the Node’s sentence does not already
begin with it (D34): “That password is too short.”, “This Node refused a request that did not come from its own
page.” (a new sentence), “That invitation could not be used.”, “That passkey was not accepted.”, “This passkey
sign-in can no longer be finished. Start again.”, “This passkey sign-in was already used. Start again.”. Every other
word before sign-in is text-identical, and the
<noscript>English is identical per paragraph. - The Doctor’s Check column shows a title above each check’s name (H2, kept in round four).
- The notices band says an approval’s subject and a matter’s type in words, and its instants in the viewer’s zone (D37).
- A bounced recipient’s bounce type is shown beside its state, in
<code>, where it was the end of the state’s title. - Tokens that were bare text are in
<code>, text unchanged: an audit action, a log event, a grant’s scope on Approvals, the SPF, DKIM and DMARC results and a DMARC policy, a Butler’syamlandjson. - Round four: the claim’s field says “Claim secret” (D35, H1); a Butler run’s state is the catalog’s word, so
awaiting_releasereads “awaiting release”, and the run a dry run reads names its trigger event in<code>(H7). - Round four’s follow-ups (3 October 2026): the Node’s claim refusals say claim secret (D35); the commands in the
claim’s secret hint and the recovery codes’ next step are in
<code>, backticks dropped (H14); the Node’s lockout sentence says “5 minutes” (H8); the notices band says “was granted a supervised read (content) of Support” (H6); a text-only body with a problem says so above the text, and a message with no body says “This message has no body.” where the panel was empty (H12); People, Rules, Butlers and Matters’ holds say “not an administrator” on a 404 only, and Matters’ list never does (H13). Outside the screens, the Node’s English for a Butler’s pause says paused and resume (G6’s Butler half).
And in layer 2b:
- Numbers are grouped: a Butler run’s counts, a rule’s daily volume, a dry run’s spend and an export object’s size (1,024 bytes). No golden held a value of 1,000 or more except the export’s.
- An unarmed breaker with no reason says “unarmed”, not “unarmed — ”.
- Setup’s delivery outcomes named Cloudflare’s event kinds “delivered, bounced, complained” in English while the Chinese
said 已受理, because this Node reads Cloudflare’s
deliveredas accepted (apps/node/worker/src/outbound/events.ts) and 送达 is inNEVER. Round three made the English say so too (D31).
Doc translations
Section titled “Doc translations”Since 3 October 2026 a few of the repository’s docs have Simplified Chinese translations, for mailda.site’s /zh-cn/
(the owner’s approval of that day). The interface’s catalog is the authority for its words; for a doc the English file
is, and the translation says so at the top of its page.
Where they live. docs/zh-cn/<name>.md, one per English file, with frontmatter naming what it was made from:
translated_from: docs/disaster-recovery.md # the English file, from the repository rootsource_sha256: f7f3980d… # the hash of the English it covers, when the translation was madesource_commit: bb5f130 # the commit it was read at, for git difftranslated_until: "## What this runbook …" # optional: the translation covers the English before this lineTwo are translated, chosen as what an operator needs first and changes least: docs/cloudflare-settings.md, whole
(what the account needs, and how either path puts it there), and docs/disaster-recovery.md up to its drill reports
(the runbook itself). The README’s opening part was translated too and then dropped: it changes about twice a day (71
commits in the month before), so its translation would have stopped every unrelated pull request until somebody
re-translated it, and the /zh-cn/ landing page already says its pitch and its status in Chinese.
docs/onboarding-journey.md, also proposed, is not translated: it is an analysis of decisions, says so in its first
line, and its operator steps are the README’s. A translation with translated_until stops before the English’s
history; the page says the rest is English and links it. apps/site/scripts/generate-docs.mjs renders each at its
English page’s path under /zh-cn/, in place of Starlight’s fallback, with its links kept inside /zh-cn/; every
other /zh-cn/docs/* page is still the English under the notice that it is not yet in your language.
The hash rule. apps/site/test/translations.test.mjs hashes the English each translation covers and fails when
that hash differs from the translation’s source_sha256, naming both files and saying to update the translation or
remove it. For a whole-file translation that is the whole file. For one with translated_until it is the English from
the top to the line before that heading, so an edit further down (a new drill report) passes, and an edit to the
translated part, or to the heading where it stops, fails. A translation nobody looked at after its English moved is
the stale doc AGENTS.md calls a landmine, and a red test is cheaper than a Chinese page saying what the English no
longer does. The same test fails a translation whose code blocks are not its English’s byte for byte, whose links
(resolved to repository paths), inline code, table rows or heading levels differ from the part of the English it
covers, that uses a NEVER phrase or 同步, or that breaks the register (the Han-Latin space and full-width
punctuation, as catalog.test.ts holds the catalog to).
Updating one. When the test names a translation:
git diff <source_commit> -- <translated_from>shows what the English did.- Change the translation to match, in the glossary’s words.
- Record the new hash as
source_sha256(node apps/site/scripts/translations.mjsprints each translation’s current one; for a whole-file translation it issha256sum <translated_from>) and the current commit assource_commit. If the heading where a partial translation stops was renamed, updatetranslated_untilfirst.
Or remove the translation: /zh-cn/ then shows that page’s English fallback again, which is honest. Never update
the hash alone without reading the diff: the hash is the record that somebody did.
The checks
Section titled “The checks”| # | What | Where |
|---|---|---|
| T1 | Missing or extra keys, keys outside an area, wrong parameters, n as a string, a literal or glued string where Text goes, an unnarrowed wire token, a plural where a string belongs: compile errors | apps/node/worker/test/client/catalog-types.test.ts |
| T2 | Placeholder parity, plural categories, Chinese punctuation and spacing, glossary terms, scoped avoid, NEVER and NEGATES, no proposed word in a released locale, areas disjoint | apps/node/worker/test/node/catalog.test.ts |
| T3 | Default-deny scan with the TypeScript checker: any literal with a letter whose contextual type is not a token type and whose position is not structural; Intl outside format.ts. The UNMIGRATED ratchet and NOT_PROSE | apps/node/worker/test/node/untranslated.test.ts |
| T6 | bootLocale(): lang, dir, title and wordmark in one step; refused storage; the flag; a Simplified Chinese browser and a stored choice; Settings lists both languages; the frame never takes the language | apps/node/worker/test/client/locale-boot.test.tsx |
| T7 | Negotiation and the boot decision (zh, zh-CN, zh-SG, zh-MY get zh-Hans; zh-TW, zh-HK, zh-MO do not); filling, plural selection and the Han-Latin space at a placeholder | apps/node/worker/test/node/locale-format.test.ts |
| T11 | The preview mechanism, in a registry with a preview in it: not offered, not negotiated, not honoured from storage, reached by the flag; not in Settings; no switch before sign-in with one offered locale | apps/node/worker/test/node/locale-preview.test.ts, apps/node/worker/test/client/locale-preview.test.tsx |
| T8 | The served tables equal the catalogs at content-tagged immutable URLs; an old tag redirects, uncached, to the current one; /app/locale.js names them and carries no app words | apps/node/worker/test/catalog-served.test.ts |
| T9 | The SC font stack’s order, tracking only through zeroable tokens, the rail under Chinese | apps/node/worker/test/node/locale-typography.test.ts |
| T10 | isComposingKey | apps/node/worker/test/node/ime.test.ts |
| T12 | A doc translation’s English has not moved since it was made (hash), and the translation keeps its code blocks, links, inline code, tables and headings, no NEVER phrase, and the register (Doc translations) | apps/site/test/translations.test.mjs |
| G | A migrated screen’s English equals the goldens taken from it before the migration, and its zh-Hans marks the Node’s words lang="en" | apps/node/worker/test/client/reader-words.test.tsx, composer-words.test.tsx, queue-words.test.tsx, ledgers-words.test.tsx (layer 2a); setup-words.test.tsx, people-words.test.tsx, matters-words.test.tsx, butlers-words.test.tsx (layer 2b) |
| M6 | The shell is preloaded, imported and its words loaded only inside handOverToShell’s try | apps/node/worker/test/node/shell-preload.test.ts |
| G (layer 3) | The pages before sign-in: English equals the goldens taken before the migration; zh-Hans shows the lockup, the switch, every refusal’s headline with the Node’s English marked, the session’s endings; <noscript> is one block per locale, English first, English word for word | apps/node/worker/test/client/preauth-words.test.ts, apps/node/worker/test/client/preauth-zh.test.ts (driver: apps/node/worker/test/client/preauth-driver.ts), apps/node/worker/test/noscript.test.ts |
| T4 | The pseudo-locale en-XA (every catalog letter accented, {placeholders} kept, +40%, in ⟦ ⟧; built from en in the test, never in LOCALES, a catalog, PREAUTH or a served table) renders every route in APP_ROUTES in the whole shell, populated, empty, with every read failed (the Node’s words, and none), the first-run gate and the notices band, then presses every button against a Node that refuses every act; and the pages before sign-in with each of their refusals. Any Latin letter outside lang="en", <code>, <kbd>, <samp>, T3’s NOT_PROSE and the fixtures’ Latin data fails. Fixture data is Greek, the Node’s own words English. Installed in zh-Hans’s slot, so <NodeWords> marks and Intl formats as for a translated locale; plurals show other only | apps/node/worker/test/client/pseudo-locale.test.tsx, pseudo-preauth.test.ts (builder and reading: apps/node/worker/test/client/pseudo-locale.ts; the Node: apps/node/worker/test/client/pseudo-node.ts) |
T4 was built in layer 3 and is green: its first run found fifteen places a screen still showed English bare (raw
tokens, raw ISO stamps, a unit, a recipient’s error unmarked, a token in a title), each fixed as the token rule
above says, and three defects that were not language (a Verify refusal read as a broken chain, an Acknowledge that
threw on an empty field, a gate that re-read the provider in a loop for a member), each fixed with its own test.
Its fixtures follow the Node’s shapes, not the screens’: the Node’s scope tokens are metadata and content, an
approval’s subject send_manifest, an audit outcome ok, refused or failed. T5, the browser sweeps under a locale, is by hand and not in CI, like the sweeps themselves: scripts/axe.mjs and
scripts/spacing.mjs take --locale <tag> before the origin, which puts the review flag on every address and reads
each migrated control’s name from the catalog the page renders from (scripts/sweep.mjs). Screenshots are by hand.