From 0c3765c8e2c7d29f4a2cc87e1a3e6da181f0cecc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 23:07:24 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(i18n):=20es=20=E7=9A=84=20preview.draft?= =?UTF-8?q?Bar=20=E7=BB=9F=E4=B8=80=20usted,=E6=A8=AA=E5=B9=85=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=9C=A8=E5=8F=91=E5=B8=83=E5=90=8E=E6=8D=A2=E4=BA=BA?= =?UTF-8?q?=E7=A7=B0=20(#3844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DraftPreviewBar` 在同一条横幅的同一行渲染两句互斥文案:有待发布变更时是 `message`,没有时是 `messageClean`。es 包里这两句的第二人称不一致 —— `message` 是 tú(`estás viendo`、`publiques`),而 `messageClean`(`ve`)与 `sampleDataBody`(`Está`、`su`、`Publíquela`)是 usted。后果:西语用户按下 「发布」之后,横幅当场从 tú 切到 usted —— 同一个组件、同一个位置、同一次会话。 这是 #3582 / #3625 之后值域盲区的第三类,三道 i18n 门禁都看不见:两条 es 译文 都正确表达了各自的 en 原意,不缺 key、不陈旧、也没有存英文;不一致是**同一 语言包内部、同一 UI 表面上的语域分裂**。`check-i18n-call-site-keys.mjs` 只问 key 在 en 存不存在;`all-locales-key-parity` 只比键名集合与占位符形状,从不读 值;`check-i18n-en-drift.mjs` 只在 en 值变化时要求九包跟改,而这两条 en 值静止 不动。 改 `message` 而不是改另外两条,依据是包里它周围本来就说 usted:es 全包普查 usted 102 : tú 30(tú 是有标记的例外,集中在 #3546 切片二明确裁定为非正式的 auth、report editor、organizations 几个邻域);同一个对象里另外三条本来就是 usted;#3546 切片五给新增的 19 个 `preview.unpublishedBar.*` / `preview.history.*` 定 usted,依据是结构孪生 `home.pendingDrafts.published` (「¡Publicado! Sus cambios están activos.」)。同一句内另外两处小分歧一并收口, 使横幅两半不再连措辞也互相矛盾: - `messageClean` 的分隔符 `:` 还原为 `—`,与 en 对齐(en 两句都以 `Draft preview — ` 起头)。这就是 `messageClean` 全部的 diff。 - 「live」用包里的 `activo` 而不是 `en producción`。这个概念在包里四处邻近位置 都写 `activo`,含同一对象内的 `publishCta`(「Publicar para verlo activo」); 而 producción 在包里留给真正的生产环境 (`environment.entitlement.planLockedBody`),所以原值不只是不一致,还有歧义。 - 共同的开头统一为 `Vista previa del borrador`,`del` 是 `messageClean` 本来 就在用的形式。 未动任何 en 值(en-drift 门禁报 0 处变化),未增删任何 key(`all-locales-key-parity` 按构造不受影响),其余九个语言包一律不碰。diff 是一个文件里的两个值。 把整个 `preview` 命名空间改成 tú 的方向经权衡后否决:改动面大得多,且会与紧邻的 `marketplace.*`(9:0 usted)与 `console.ai.*` 冲突。因此 `preview.empty.notReadyDescription` 刻意保持 tú —— 它是另一个表面(空态,不是这条 横幅),不存在会话中途在用户眼前切换的问题,#3844 正文已记录在案。「同一命名空间 一个语域」的门禁刻意**不做**:识别 usted / tú 需要真正的形态学(本次普查用的临时 正则就把命令式 `Revisa` 误算成 tú 标记),而这种门禁要police的邻域边界本身得人来划 —— 即 #3546 切片二「同一规则、不同答案」的先例。改为新增 `draftBar-es-register-3844.test.ts`,把四个 es 值连同两条 en 原文逐字节钉住, 这样将来任何一次 en 改写都会在改写它的那个 PR 里当场红,而不是悄悄把译文孤立掉。 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt --- .changeset/draftbar-es-register-3844.md | 59 ++++++ .../draftBar-es-register-3844.test.ts | 192 ++++++++++++++++++ packages/i18n/src/locales/es.ts | 4 +- 3 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 .changeset/draftbar-es-register-3844.md create mode 100644 packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts diff --git a/.changeset/draftbar-es-register-3844.md b/.changeset/draftbar-es-register-3844.md new file mode 100644 index 000000000..6951893c8 --- /dev/null +++ b/.changeset/draftbar-es-register-3844.md @@ -0,0 +1,59 @@ +--- +'@object-ui/i18n': patch +--- + +`preview.draftBar` speaks one second person in `es` — the draft-preview banner no +longer switches from tú to usted when a Spanish user publishes (#3844) + +`DraftPreviewBar` renders two mutually exclusive sentences in the same strip of +the same banner: `message` while there are unpublished changes, `messageClean` +once there are none. In `es` the two disagreed on register — `message` was tú +(`estás viendo`, `publiques`) while `messageClean` (`ve`) and `sampleDataBody` +(`Está`, `su`, `Publíquela`) were usted. So a Spanish user who pressed Publish +watched the banner change person: same component, same position, same session. + +This is a third defect class in the value-domain blind spot behind #3582 and +#3625, and no gate in the repo can see it. Both `es` values are correct +translations of their `en` sentences — nothing is missing, nothing is stale, and +nothing holds English. The inconsistency is *internal to one pack, on one UI +surface*: `scripts/check-i18n-call-site-keys.mjs` only asks whether a key exists +in `en`, `all-locales-key-parity` compares key sets and placeholder shapes and +never reads values, and `scripts/check-i18n-en-drift.mjs` only fires when an `en` +value moves — these two `en` values never moved. + +`message` is the value that changes, because usted is what the pack already says +everywhere around it: the `es` pack censuses 102 usted markers to 30 tú (tú being +the marked exception, concentrated in the auth, report-editor and organizations +neighbourhoods that #3546 slice two deliberately ruled informal); the other three +strings of this same object were already usted; and #3546 slice five gave the 19 +new `preview.unpublishedBar.*` / `preview.history.*` keys usted on the strength +of `home.pendingDrafts.published` ("¡Publicado! Sus cambios están activos."). Two +smaller divergences inside the same sentence are closed with it, so the banner's +two halves stop disagreeing about wording as well as person: + +- **`—` instead of `:`** in `messageClean`, matching `en`, where both sentences + open `Draft preview — `. That is the whole of `messageClean`'s diff. +- **`activo` instead of `en producción`** for "live". The pack spells this concept + `activo` in four neighbouring places including `publishCta` in this very object + ("Publicar para verlo activo"), and it reserves *producción* for the actual + production environment (`environment.entitlement.planLockedBody`), so the + outlier was ambiguous as well as inconsistent. +- **`Vista previa del borrador`** as the shared opening, `del` being the form + `messageClean` already used. + +No `en` value changes (the en-drift gate reports 0), no key is added or removed +(so `all-locales-key-parity` is untouched by construction), and the nine other +packs are not touched. The diff is two values in one file. + +Re-voicing the whole `preview` namespace to tú was considered and rejected — a +much larger change that would collide with the adjacent `marketplace.*` (9:0 +usted) and `console.ai.*`. `preview.empty.notReadyDescription` therefore stays +tú on purpose: it is a different surface, nothing switches under the user +mid-session, and #3844's body records it. A gate that checks "one register per +namespace" is deliberately **not** here: recognising usted vs tú needs real +morphology (the ad-hoc regex used to take the census above already miscounted the +imperative `Revisa` as a tú marker), and the neighbourhood boundary such a gate +would police is human judgement — #3546 slice two's "same rule, different +answer". A new `draftBar-es-register-3844.test.ts` pins the four `es` values byte +for byte instead, plus the `en` literals, so a future reword of either `en` +sentence fails in the same PR that reworded it. diff --git a/packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts b/packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts new file mode 100644 index 000000000..c1bdc2df6 --- /dev/null +++ b/packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts @@ -0,0 +1,192 @@ +/** + * `preview.draftBar` speaks ONE second person in `es` — usted (objectui#3844). + * + * ## The defect this pins + * + * `DraftPreviewBar` renders two mutually exclusive sentences in the same strip + * of the same banner: `message` when there are unpublished changes, + * `messageClean` when there are none. In `es` the two disagreed on register — + * `message` was tú (`estás viendo`, `publiques`) while `messageClean` (`ve`) + * and `sampleDataBody` (`Está`, `su`, `Publíquela`) were usted. A Spanish user + * who pressed Publish watched the banner switch from tú to usted: same + * component, same position, same session. + * + * ## Why usted is the answer here (the ruling, not a preference) + * + * Three independent readings, all measured rather than guessed: + * + * 1. **Pack default.** The `es` pack censuses 102 usted markers against 30 + * tú. tú is the marked exception, concentrated in auth sign-in/sign-up, + * the report editor and organizations — neighbourhoods objectui#3546 + * slice two deliberately ruled informal. `preview` is not one of them. + * 2. **Same banner, same voice.** The other two strings of this very object + * (`messageClean`, `sampleDataBody`) were already usted, and so is + * `publishCta`. Aligning the one outlier is a strictly smaller change + * than re-voicing the namespace, and it is the change that removes the + * user-visible switch. + * 3. **The sister banner.** objectui#3546 slice five gave the 19 new + * `preview.unpublishedBar.*` / `preview.history.*` keys usted, on the + * strength of `home.pendingDrafts.published` ("¡Publicado! Sus cambios + * están activos."), the structural twin of + * `preview.unpublishedBar.published`. Leaving `draftBar.message` tú would + * have made the older banner disagree with its freshly-translated sister. + * + * Direction B (re-voicing all of `preview` to tú) was rejected: it is a much + * larger change and would collide with the adjacent `marketplace.*` (9:0 + * usted) and `console.ai.*`. + * + * ## What this file deliberately does NOT do + * + * **No morphological register detection.** objectui#3844 evaluated a gate that + * checks "one register per namespace" and argued it down: recognising usted vs + * tú needs real morphology, and the ad-hoc regex used to take the census above + * already miscounted the imperative `Revisa` as a tú marker. Worse, the + * boundary such a gate would police — which neighbourhood is informal — is + * human judgement (slice two's "same rule, different answer" precedent), so a + * gate could only ever check consistency *within* a boundary it cannot draw. + * This file therefore pins **these four literal values, byte for byte**, and + * generalises to nothing. The retired-spelling checks below are exact + * substrings lifted from the pre-fix value of this one key — the same + * technique as `viewReadonlyTooltip-semantics-3625.test.ts` — not a register + * rule in disguise. + * + * Scope note: `preview.empty.notReadyDescription` is still tú and is left + * that way on purpose. It is a different surface (an empty state, not this + * banner), nothing switches under the user mid-session, and re-voicing the + * whole namespace is exactly the rejected direction B. objectui#3844's body + * records it. + * + * Related: objectui#3546 (slice five measured this while ruling es register), + * objectui#3810 / objectui#3582 / objectui#3625 (the other value-domain blind + * spots), objectui#3530 (call-site gate), objectui#3650 (en-drift gate). None + * of the three i18n gates can see this defect: the call-site gate only asks + * whether a key exists in `en`, `all-locales-key-parity` compares key names + * and placeholder shapes and never reads values, and the en-drift gate only + * fires when an `en` value moves — these two `en` values never moved. + */ +import { describe, it, expect } from 'vitest'; +import { builtInLocales } from '../locales'; + +type Bag = Record; +const draftBar = (lang: string) => (builtInLocales[lang] as Bag)?.preview?.draftBar as Bag; +const at = (lang: string, key: string) => draftBar(lang)?.[key] as string; + +/** The four `es` values this file owns, at their post-objectui#3844 final bytes. */ +const ES_FINAL = { + message: + 'Vista previa del borrador — está viendo cambios sin publicar. Nada de lo que ve aquí está activo hasta que publique.', + messageClean: + 'Vista previa del borrador — no hay cambios sin publicar; todo lo que ve aquí ya está activo.', + sampleDataBody: + 'Está viendo la estructura de su aplicación. Publíquela para cargar registros de ejemplo y activarla.', + publishCta: 'Publicar para verlo activo', +} as const; + +/** `en`'s current wording. If either moves, the `es` pins above must be revisited. */ +const EN_CURRENT = { + message: 'Draft preview — you are seeing unpublished changes. Nothing here is live until you publish.', + messageClean: 'Draft preview — no unpublished changes; everything here is already live.', +} as const; + +/** + * Exact substrings the pre-fix `es` value held, scoped to this key. Each names + * one sub-change of the fix, so a red points at which one came back: + * - the tú morphology (`estás viendo`, `publiques`); + * - `en producción` for "live", which the pack spells `activo` (see + * `publishCta` in this same object, `home.pendingDrafts.published`) and + * which additionally collides with the pack's *production environment* + * sense in `environment.entitlement.planLockedBody`; + * - `Vista previa de borrador`, the prefix that differed from + * `messageClean`'s `Vista previa del borrador` so the banner's two halves + * opened with different words where `en`'s both open "Draft preview — ". + */ +const RETIRED_SPELLINGS = [ + 'estás viendo', + 'publiques', + 'en producción', + 'Vista previa de borrador', +] as const; + +const EM_DASH = '—'; +const BANNER_PREFIX = `Vista previa del borrador ${EM_DASH} `; + +describe('preview.draftBar es register (objectui#3844)', () => { + it('the keys this file pins are present and non-empty', () => { + // Anti-empty-green: every assertion below reads one of these four values, + // and a renamed or dropped key would otherwise let the negative checks + // pass on `undefined`. + for (const key of Object.keys(ES_FINAL)) { + const value = at('es', key); + expect(typeof value, `es preview.draftBar.${key}`).toBe('string'); + expect(value.trim().length, `es preview.draftBar.${key} is empty`).toBeGreaterThan(0); + } + for (const key of Object.keys(EN_CURRENT)) { + expect(typeof at('en', key), `en preview.draftBar.${key}`).toBe('string'); + } + }); + + it.each(Object.entries(ES_FINAL))('es preview.draftBar.%s is exactly the ruled value', (key, expected) => { + // THE assertion. Byte-exact on purpose: register lives in individual + // morphemes (está vs estás, publique vs publiques), so a substring or + // regex check would let a partial re-edit through. + expect(at('es', key)).toBe(expected); + }); + + it('no retired spelling of the tú/en-producción wording is back', () => { + // Red before the fix on `message` for three of these four literals; kept + // as the named tripwire so a future re-edit that reintroduces one fails + // with the offending literal in the message rather than as an opaque + // byte diff. + for (const key of Object.keys(ES_FINAL)) { + const value = at('es', key); + for (const retired of RETIRED_SPELLINGS) { + expect(value.includes(retired), `es preview.draftBar.${key} reintroduced "${retired}"`).toBe( + false, + ); + } + } + }); + + it('both halves of the banner open with the same words and the same em-dash', () => { + // The structural half of the fix, stated as its own claim: `en` opens both + // sentences "Draft preview — ", so `es` must open both identically too. + // `messageClean` used to separate with a colon, which made the banner + // change punctuation as well as person when the user published. + expect(at('es', 'message').startsWith(BANNER_PREFIX)).toBe(true); + expect(at('es', 'messageClean').startsWith(BANNER_PREFIX)).toBe(true); + expect(at('es', 'messageClean')).not.toContain('borrador:'); + for (const key of ['message', 'messageClean']) { + expect(at('en', key).startsWith(`Draft preview ${EM_DASH} `), `en ${key} prefix`).toBe(true); + } + }); + + it('all four values spell "live" with the pack\'s own word', () => { + // "Same concept, same word" — the stem covers `activo` (message, + // messageClean, publishCta) and `activarla` (sampleDataBody). Paired with + // the `en producción` literal above: this side proves the replacement is + // present, that side proves the outlier is gone. + for (const key of Object.keys(ES_FINAL)) { + expect(at('es', key), `es preview.draftBar.${key} lost the pack's word for "live"`).toContain( + 'activ', + ); + } + }); + + it('en still says what the es values were translated against', () => { + // objectui#3650's gate only fires when an `en` value changes, and it asks + // the nine packs to follow — it cannot know this `es` ruling. This line + // goes red in the same PR that rewords either `en` sentence, which is what + // forces the usted rewrite above to be re-read then. + expect(at('en', 'message')).toBe(EN_CURRENT.message); + expect(at('en', 'messageClean')).toBe(EN_CURRENT.messageClean); + }); + + it('es serves neither en sentence verbatim', () => { + // The objectui#3582 failure mode (English pasted into a translation pack) + // applied to these keys. Green before this fix too — recorded as a guard + // against a future paste, not as evidence for objectui#3844. + const english = Object.values(EN_CURRENT) as string[]; + expect(english).not.toContain(at('es', 'message')); + expect(english).not.toContain(at('es', 'messageClean')); + }); +}); diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 95f0ff8a2..af7d056eb 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -3288,11 +3288,11 @@ const es = { retry: 'Reintentar', }, draftBar: { - messageClean: "Vista previa del borrador: no hay cambios sin publicar; todo lo que ve aquí ya está activo.", + messageClean: "Vista previa del borrador — no hay cambios sin publicar; todo lo que ve aquí ya está activo.", sampleDataTitle: "Los datos de ejemplo aparecen tras publicar", sampleDataBody: "Está viendo la estructura de su aplicación. Publíquela para cargar registros de ejemplo y activarla.", publishCta: "Publicar para verlo activo", - message: 'Vista previa de borrador — estás viendo cambios sin publicar. Nada está en producción hasta que publiques.', + message: 'Vista previa del borrador — está viendo cambios sin publicar. Nada de lo que ve aquí está activo hasta que publique.', publish: 'Publicar', publishing: 'Publicando…', exit: 'Salir de la vista previa', From 5a5565905c8f26e46e403615b32af08b72d2de35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 23:22:01 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(i18n):=20=E6=9B=B4=E6=AD=A3=E9=92=89?= =?UTF-8?q?=E5=AD=90=E6=B3=A8=E9=87=8A=E9=87=8C=E3=80=8C=E6=AD=A3=E5=88=99?= =?UTF-8?q?=E8=AF=AF=E5=88=A4=E3=80=8D=E7=9A=84=E6=9C=BA=E5=88=B6,?= =?UTF-8?q?=E5=B9=B6=E6=8A=8A=E5=90=8C=E5=B1=8F=E6=AE=8B=E7=95=99=E7=AB=8B?= =?UTF-8?q?=E4=B8=BA=20#3875?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自查发现第一版注释照抄了 #3844 正文的一句未经复核的说法(「普查正则把命令式 `Revisa` 误算成 tú 标记」),而实测机制不是这样 —— 不该把一句没验证过的断言 固化进永久注释: - es.ts:1918 `auth.forgotPassword.successTitle`(`Revisa tu correo electrónico`) 的 `Revisa` 是**真的** tú 命令式,不是误判; - 真正的假阳性在 es.ts:1365 `console.ai.empty.build` (`…y usted revisa y publica.`)—— 那里的 `revisa` 是带显式主语 `usted` 的 陈述式,是 **usted** 串。同一个 token 在两处敬称相反,任何字符串正则都分不开。 - 反向误判同样容易:粗正则扫 `approvalsInbox` 得到 5 个「usted」全是假阳性 (`puede` 说的是第三人称的申请人 / 被指派人,`No se puede determinar` 是无人称), 而该命名空间的第二人称称呼实为一致 tú(9 条)—— 它属于切片二「按邻域裁定」的 正当情形,注释里写明以免下一个人误伤。 这组实测比原说法更有力地支持否决方向 C(敬称门禁),故写进注释。 另一处更正:`preview.empty.notReadyDescription` 原注释写「不存在会话中途在用户 眼前切换」,过弱且不准 —— `PreviewDraftEmptyState`(`AppContent.tsx`, `previewDrafts` 且应用未就绪时返回)渲染在 `DraftPreviewBar` (`ConsoleLayout.tsx`)**下方的内容区**,两者会**同屏**:横幅 usted、下面的空态 tú。这比 #3844 本身的先后切换更尖,已单独立单 #3875(未认领、未打标,交 PM 分诊), 注释与 changeset 一并改为指向它。本单仍只改 draftBar 那两个值,不越界。 es.ts 两个值一字未动(本次提交只碰注释与 changeset 文案)。 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt --- .changeset/draftbar-es-register-3844.md | 26 ++++++++----- .../draftBar-es-register-3844.test.ts | 39 +++++++++++++------ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/.changeset/draftbar-es-register-3844.md b/.changeset/draftbar-es-register-3844.md index 6951893c8..8de293522 100644 --- a/.changeset/draftbar-es-register-3844.md +++ b/.changeset/draftbar-es-register-3844.md @@ -47,13 +47,19 @@ packs are not touched. The diff is two values in one file. Re-voicing the whole `preview` namespace to tú was considered and rejected — a much larger change that would collide with the adjacent `marketplace.*` (9:0 -usted) and `console.ai.*`. `preview.empty.notReadyDescription` therefore stays -tú on purpose: it is a different surface, nothing switches under the user -mid-session, and #3844's body records it. A gate that checks "one register per -namespace" is deliberately **not** here: recognising usted vs tú needs real -morphology (the ad-hoc regex used to take the census above already miscounted the -imperative `Revisa` as a tú marker), and the neighbourhood boundary such a gate -would police is human judgement — #3546 slice two's "same rule, different -answer". A new `draftBar-es-register-3844.test.ts` pins the four `es` values byte -for byte instead, plus the `en` literals, so a future reword of either `en` -sentence fails in the same PR that reworded it. +usted) and `console.ai.*`. `preview.empty.notReadyDescription` therefore stays tú +here, since #3844's ruling is this banner only; it is filed as #3875 rather than +waved through, because `PreviewDraftEmptyState` renders *underneath* +`DraftPreviewBar` and so those two show usted and tú on screen at the same time. +After this change the `preview` namespace is 23 usted to that 1 tú. + +A gate that checks "one register per namespace" is deliberately **not** here: +recognising usted vs tú needs real morphology, and token matching demonstrably +cannot — in this pack the token `revisa` is usted in `console.ai.empty.build` +(es.ts:1365, "…y usted revisa y publica.") and tú in +`auth.forgotPassword.successTitle` (es.ts:1918, "Revisa tu correo electrónico"), +the same eight letters in opposite registers. The neighbourhood boundary such a +gate would police is human judgement anyway — #3546 slice two's "same rule, +different answer". A new `draftBar-es-register-3844.test.ts` pins the four `es` +values byte for byte instead, plus the `en` literals, so a future reword of +either `en` sentence fails in the same PR that reworded it. diff --git a/packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts b/packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts index c1bdc2df6..439f00a84 100644 --- a/packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts +++ b/packages/i18n/src/__tests__/draftBar-es-register-3844.test.ts @@ -38,23 +38,40 @@ * ## What this file deliberately does NOT do * * **No morphological register detection.** objectui#3844 evaluated a gate that - * checks "one register per namespace" and argued it down: recognising usted vs - * tú needs real morphology, and the ad-hoc regex used to take the census above - * already miscounted the imperative `Revisa` as a tú marker. Worse, the - * boundary such a gate would police — which neighbourhood is informal — is - * human judgement (slice two's "same rule, different answer" precedent), so a - * gate could only ever check consistency *within* a boundary it cannot draw. + * checks "one register per namespace" and argued it down. Recognising usted vs + * tú needs real morphology, and token matching demonstrably cannot: in this + * very pack the token `revisa` is **usted** in `console.ai.empty.build` + * (es.ts:1365, "…y usted revisa y publica.", present indicative with an + * explicit `usted`) and **tú** in `auth.forgotPassword.successTitle` + * (es.ts:1918, "Revisa tu correo electrónico", imperative). Same eight + * letters, opposite registers, and no regex over the string can tell them + * apart. The reverse error is just as easy: third-person and impersonal forms + * read as usted markers — a crude `puede|su|…` scan over `approvalsInbox` + * returns five "usted" hits that are all about *el solicitante* or are + * impersonal ("No se puede determinar la identidad"), in a namespace whose + * second-person address is in fact uniformly tú (nine strings). objectui#3844 + * recorded this hazard as "the census regex miscounted `Revisa`"; the + * measurement above is the precise version — the miscount is the usted + * indicative at :1365, not an imperative. Worse, the boundary such a gate + * would police — which neighbourhood is informal — is human judgement (slice + * two's "same rule, different answer" precedent), so a gate could only ever + * check consistency *within* a boundary it cannot draw. * This file therefore pins **these four literal values, byte for byte**, and * generalises to nothing. The retired-spelling checks below are exact * substrings lifted from the pre-fix value of this one key — the same * technique as `viewReadonlyTooltip-semantics-3625.test.ts` — not a register * rule in disguise. * - * Scope note: `preview.empty.notReadyDescription` is still tú and is left - * that way on purpose. It is a different surface (an empty state, not this - * banner), nothing switches under the user mid-session, and re-voicing the - * whole namespace is exactly the rejected direction B. objectui#3844's body - * records it. + * Scope note: `preview.empty.notReadyDescription` (es.ts:3286, `Revisa`) is + * still tú and is left that way here, because re-voicing the namespace is the + * rejected direction B and objectui#3844's ruling is this banner only. It is + * **not** harmless, and this file does not pin it: `PreviewDraftEmptyState` + * (`AppContent.tsx`, returned while `previewDrafts` and the app is not yet + * staged) renders in the content area *underneath* `DraftPreviewBar` + * (`ConsoleLayout.tsx`), so those two are on screen at the SAME time — usted in + * the bar, tú in the pane below it. That is sharper than objectui#3844's own + * sequential switch and is filed separately as objectui#3875. After this change + * the `preview` namespace is 23 usted to that 1 tú. * * Related: objectui#3546 (slice five measured this while ruling es register), * objectui#3810 / objectui#3582 / objectui#3625 (the other value-domain blind