From 02c00773eb51e328dc1ce339654d3f7fc3a391da Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 16:50:03 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(i18n):=20de=20approvalsInbox=20?= =?UTF-8?q?=E4=B8=89=E5=80=BC=E7=94=A8=E5=BE=B7=E8=AF=AD=E5=BC=95=E5=8F=B7?= =?UTF-8?q?=20=E2=80=9E=E2=80=A6=E2=80=9C=20=E8=80=8C=E9=9D=9E=20ASCII=20?= =?UTF-8?q?=E7=9B=B4=E5=BC=95=E5=8F=B7=20(#3919)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `approvalsInbox.rejectOneTitle` / `inlineApproved` / `inlineRejected` 三个值在 标题两侧都用 ASCII 直引号,而同一命名空间的兄弟 `approveOneTitle` 用的是正确的 德语引号(U+201E 开、U+201C 闭)。德语审批者因此在同一屏、同一个操作对里看到 两种引号排印:批准确认是 „…“,驳回确认与两个 inline toast 是 "…"。 `„…“` 是 de 包自己的多数派(50 处配对)也是 DUDEN R11 的规定,所以这三个值是 离群值。只改值域排印,键名与 `{{title}}` 占位符不动;en 侧按设计两侧都是 ASCII, 不受影响。 这与 #3876 是不同的缺陷形态:#3876 是德语开引号配了 ASCII 闭引号,而这三个值 两侧都是 ASCII、自身"配平",所以 #3876 留下的配对不变量结构上看不到它们 —— 那 个扫描器从每个 `„` 向前找第一个引号字符,而这三个值里根本没有 `„`。 de 包直引号清零后,把 `de-quote-pairing-3876.test.ts` 里的显式三键清单升级为更 强的形态:断言 de 包任何值都不含 U+0022。它同时覆盖两种缺陷形态,且不需要逐键 维护 —— 以后新落地的值带上打字机引号会直接按键名翻红。清零断言配了反空转守卫 (en 包同一谓词必须仍能找到 40 个值),避免"因为什么都没产出而绿"。同时新增 approvalsInbox 四值的逐字节钉子,因为清零断言本身不再点名任何键。 包内计数 47/47/0/6 → 50/50/0/0,三处字面量随之更新。 --- .changeset/de-approvals-inbox-quotes-3919.md | 41 +++++++++ .../__tests__/de-quote-pairing-3876.test.ts | 91 +++++++++++++++---- packages/i18n/src/locales/de.ts | 6 +- 3 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 .changeset/de-approvals-inbox-quotes-3919.md diff --git a/.changeset/de-approvals-inbox-quotes-3919.md b/.changeset/de-approvals-inbox-quotes-3919.md new file mode 100644 index 0000000000..310104594c --- /dev/null +++ b/.changeset/de-approvals-inbox-quotes-3919.md @@ -0,0 +1,41 @@ +--- +"@object-ui/i18n": patch +--- + +`de` approvals inbox no longer shows two quote typographies on one screen + +Three `approvalsInbox` values quoted the record title with ASCII straight quotes +on **both** sides while their own sibling `approvalsInbox.approveOneTitle` used +the correct German pair (U+201E low-9 opener, U+201C closer): + +| key | before | after | +| --- | --- | --- | +| `approvalsInbox.rejectOneTitle` | `"{{title}}" ablehnen?` | `„{{title}}“ ablehnen?` | +| `approvalsInbox.inlineApproved` | `"{{title}}" genehmigt` | `„{{title}}“ genehmigt` | +| `approvalsInbox.inlineRejected` | `"{{title}}" abgelehnt` | `„{{title}}“ abgelehnt` | + +A German approver therefore met both typographies in a single screen and inside a +single operation pair — German quotes on the approve confirmation, typewriter +quotes on the reject confirmation and on both inline toasts. `„…“` is the pack's +own majority (50 paired spans) and DUDEN R11, so the three values were the +outlier. Keys and the `{{title}}` placeholder are unchanged; this is a +value-domain typography fix, and `en` (ASCII on both sides by design) is +untouched. + +This is a **different defect shape** from objectui#3876, which paired a German +opener with an ASCII closer. Because these three were ASCII on both sides they +were self-consistent, so the pairing invariant objectui#3876 left behind could +not see them — it scans forward from each `„`, and there was no `„` in them to +scan from. The pack's straight-quote census being empty now lets that pin become +strictly stronger: `de-quote-pairing-3876.test.ts` asserted an explicit +three-key allowlist and now asserts that **no `de` value holds a U+0022 at all**, +which covers both defect shapes and needs no per-key maintenance as new values +land. The pack census moves from `„` 47 / `“` 47 / `”` 0 / `"` 6 to +`„` 50 / `“` 50 / `”` 0 / `"` 0. + +None of the three i18n gates could have caught this: `all-locales-key-parity` +compares key sets and placeholder shapes, `check-i18n-call-site-keys.mjs` only +asks whether a key resolves, and `check-i18n-en-drift.mjs` fires on `en` value +changes — these values were wrong from the day they landed, so no drift event +ever existed. All three are value-blind by design, which is why the invariant +lives in a test. diff --git a/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts b/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts index 3b08c83926..ace9e3fe0e 100644 --- a/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts +++ b/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts @@ -52,6 +52,24 @@ * values used to be the surplus, and an assertion that passes because nothing * is produced is exactly the shape this suite exists to avoid. * + * ## objectui#3919 took the last straight quotes out — the pin is now "zero" + * + * Three `approvalsInbox` values (`rejectOneTitle`, `inlineApproved`, + * `inlineRejected`) quoted with ASCII on **both** sides. Being self-consistent, + * they were a second defect shape that the pairing scan below structurally cannot + * see: there is no `„` in them to scan forward from. This file recorded them as + * an explicit three-key list while that finding waited its turn, and a German + * approver meanwhile read two typographies on one screen — `„…“` on the approve + * confirmation, `"…"` on the reject one and on both inline toasts. + * + * objectui#3919 germanised the three, which takes the census to `„` 50, `“` 50, + * `”` 0, `"` **0** and lets that list become the strictly stronger invariant it + * now is: **no value in the de pack holds a U+0022 at all**. Stronger because it + * catches both defect shapes — one side of a mismatched pair and both sides of a + * self-consistent ASCII pair — and it needs no per-key maintenance: a new value + * arriving with a typewriter quote fails by key name without anyone editing a + * list. The three counts below moved 47 → 50 with that change. + * * ## Why the three i18n gates cannot see any of this * * `all-locales-key-parity` compares key sets and placeholder shapes, @@ -233,8 +251,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight } expect(found, `„…" mismatches:\n${found.join('\n')}`).toEqual([]); // 45 at objectui#3876's landing, 47 once objectui#3920 gave - // `grid.import.savedMappingHint` / `savedMappingPreviewNote` German quotes. - expect(okSpans, 'correctly paired spans').toBe(47); + // `grid.import.savedMappingHint` / `savedMappingPreviewNote` German quotes, + // 50 once objectui#3919 germanised the three `approvalsInbox` values. + expect(okSpans, 'correctly paired spans').toBe(50); }); it('keeps the count identity that replaces the card’s count(„) === count(“)', () => { @@ -244,11 +263,12 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight const rdq = count(whole, RDQ); // 45 / 47 / 2 at objectui#3876's landing; 47 / 47 / 0 after objectui#3920 - // translated the two English values. See the header for why the naive - // equality was false on the file #3876 left behind — and note that it is + // translated the two English values; 50 / 50 / 0 after objectui#3919 gave the + // three `approvalsInbox` values German quotes. See the header for why the + // naive equality was false on the file #3876 left behind — and note that it is // now true for a *different* reason (rdq went to zero), which is why the // identity below is asserted as arithmetic rather than as `close === open`. - expect({ open, close, rdq }).toEqual({ open: 47, close: 47, rdq: 0 }); + expect({ open, close, rdq }).toEqual({ open: 50, close: 50, rdq: 0 }); // The durable shape: every „ closed by a “, every surplus “ an English // opener answered by a ”. Survived translating the two English values. expect(close).toBe(open + rdq); @@ -295,18 +315,57 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight expect(count(JSON.stringify(builtInLocales.en), OPEN)).toBe(0); }); - it('records the remaining straight quotes in de: the approvalsInbox trio, filed separately', () => { - // A different defect shape, deliberately NOT fixed here (this issue is the - // mismatched pair): these three quote with ASCII on BOTH sides while their - // own sibling `approvalsInbox.approveOneTitle` is correctly „…“. Pinned so - // the number cannot drift unnoticed while that finding waits its turn — and - // so a future fix has to come back and update this list. + it('holds pack-wide: no de value quotes with a U+0022 at all (objectui#3919)', () => { + // This used to be an explicit three-key list of the `approvalsInbox` values + // that were still ASCII-quoted on both sides. objectui#3919 fixed them, so the + // list collapses to the strictly stronger form: `„…“` is this pack's + // convention, therefore ANY U+0022 in a German value is the defect — whether + // it is one side of a mismatched pair (the objectui#3876 shape, which the + // scanner above also catches) or both sides of a self-consistent ASCII pair + // (the objectui#3919 shape, which the scanner structurally cannot catch). + // No per-key list to maintain: the next such value fails here by name. const straight = DE.filter(([, v]) => v.includes(STRAIGHT)).map(([k]) => k); - expect(straight).toEqual([ - 'approvalsInbox.rejectOneTitle', - 'approvalsInbox.inlineApproved', - 'approvalsInbox.inlineRejected', - ]); + expect(straight, `de values still quoting with U+0022:\n${straight.join('\n')}`).toEqual([]); + + // `toEqual([])` is precisely the assertion shape that also passes when nothing + // was produced, so the predicate must be shown to still be able to find + // something. `en` quotes with ASCII by design (see the `it` below), so the + // same filter over `en` returns a long list; a broken `flatten`, an emptied + // pack or a renamed export would take that to zero and fail here instead of + // reading green above. 40 en values at objectui#3919's landing. + expect(DE.length, 'de pack looks empty — the scan would be vacuous').toBeGreaterThan(2000); + const enStraight = flatten(builtInLocales.en).filter(([, v]) => v.includes(STRAIGHT)); + expect( + enStraight.length, + 'the U+0022 predicate finds nothing in en either — it is broken, not the pack clean', + ).toBeGreaterThan(20); + }); + + it('pins the approvalsInbox quartet objectui#3919 converged, byte-for-byte', () => { + // The four sentences a German approver meets in one screen: the approve and + // reject confirmation titles, and the twin inline toasts. Three were ASCII on + // both sides while `approveOneTitle` alone was correct — one operation pair, + // two typographies, which is the whole substance of the card. Pinned whole + // here because the `it` above deliberately no longer names any key, and an + // invariant that names nothing would stop recording what this fix touched. expect(at(builtInLocales.de, 'approvalsInbox.approveOneTitle')).toBe('„{{title}}“ genehmigen?'); + expect(at(builtInLocales.de, 'approvalsInbox.rejectOneTitle')).toBe('„{{title}}“ ablehnen?'); + expect(at(builtInLocales.de, 'approvalsInbox.inlineApproved')).toBe('„{{title}}“ genehmigt'); + expect(at(builtInLocales.de, 'approvalsInbox.inlineRejected')).toBe('„{{title}}“ abgelehnt'); + + // The fix was value-domain typography only. `{{title}}` is what makes these + // sentences work at runtime and `all-locales-key-parity` compares placeholder + // shapes, so losing one would break far more than typography. + for (const key of ['approveOneTitle', 'rejectOneTitle', 'inlineApproved', 'inlineRejected']) { + const v = at(builtInLocales.de, `approvalsInbox.${key}`) as string; + expect(v.includes('{{title}}'), `de approvalsInbox.${key} lost {{title}}`).toBe(true); + expect(v.includes('„{{title}}“'), `de approvalsInbox.${key} span is not paired`).toBe(true); + } + + // `en` is untouched and still ASCII on both sides — the shape de diverges from + // on purpose, and the cheap local proof this fix did not leak across packs. + expect(at(builtInLocales.en, 'approvalsInbox.rejectOneTitle')).toBe('Reject "{{title}}"?'); + expect(at(builtInLocales.en, 'approvalsInbox.inlineApproved')).toBe('Approved "{{title}}"'); + expect(at(builtInLocales.en, 'approvalsInbox.inlineRejected')).toBe('Rejected "{{title}}"'); }); }); diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 6c7fd0564b..8ac7e54115 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -3276,14 +3276,14 @@ const de = { bulkApproved: '{{count}} Anfragen genehmigt', bulkRejected: '{{count}} Anfragen abgelehnt', bulkPartial: '{{ok}} erfolgreich, {{fail}} fehlgeschlagen: {{which}}', - rejectOneTitle: '"{{title}}" ablehnen?', + rejectOneTitle: '„{{title}}“ ablehnen?', rejectOneBody: 'Die Anfrage wird abgelehnt und der Antragsteller benachrichtigt.', rejectTitle: 'Diese Anfrage ablehnen?', rejectBody: 'Die Anfrage wird als abgelehnt markiert und der Antragsteller benachrichtigt.', recallTitle: 'Diese Anfrage zurückziehen?', recallBody: 'Genehmiger können danach nicht mehr handeln; der Datensatz wird entsperrt.', - inlineApproved: '"{{title}}" genehmigt', - inlineRejected: '"{{title}}" abgelehnt', + inlineApproved: '„{{title}}“ genehmigt', + inlineRejected: '„{{title}}“ abgelehnt', approvedFinal: 'Genehmigt', approvedWaiting: 'Genehmigt — wartet auf die übrigen Genehmiger', rejectedToast: 'Abgelehnt', From f7204b0f92a1c7797e7c4023f01a9668f3b86618 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 16:54:25 +0000 Subject: [PATCH 2/3] =?UTF-8?q?docs(i18n):=20#3920=20=E6=99=AE=E6=9F=A5?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=20de=20=E8=A1=8C=E6=B3=A8=E6=98=8E=20ASCII?= =?UTF-8?q?=203=20=E5=8D=B3=20#3919=20=E4=B8=89=E5=80=BC,=E7=8E=B0?= =?UTF-8?q?=E5=B7=B2=E5=BD=92=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 那张按包统计 {{placeholder}} 包裹风格的普查表里,de 行的「ASCII 3」正是 approvalsInbox rejectOneTitle / inlineApproved / inlineRejected 这三个值。本 分支把它们德语化后该列归零,de 的裁定从 38 比 3 变为一致。 刻意不在此处给出新的 „…“ 计数:该表是在**排除** #3920 移除的两处英文残留的 前提下测得的,在当前包上重测是另一种口径,两者不可比(实测重算会得到 43 而非 38+3=41,差额正是那两处已翻译的 grid.import 值)。表保持原样作为当时裁定的 依据;活的不变量在 de-quote-pairing-3876.test.ts。 --- .../gridImportSavedMapping-i18n-3920.test.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/i18n/src/__tests__/gridImportSavedMapping-i18n-3920.test.ts b/packages/i18n/src/__tests__/gridImportSavedMapping-i18n-3920.test.ts index c1c7e178d1..00431d4ca3 100644 --- a/packages/i18n/src/__tests__/gridImportSavedMapping-i18n-3920.test.ts +++ b/packages/i18n/src/__tests__/gridImportSavedMapping-i18n-3920.test.ts @@ -67,11 +67,22 @@ * objectui#3546 slices used ASCII, older values used guillemets), which is why * the majority and not a meaning-based rule is the tiebreak here. * + * The `de` row's "ASCII 3" was **the objectui#3919 trio** — `approvalsInbox` + * `rejectOneTitle` / `inlineApproved` / `inlineRejected`, the only ASCII-quoted + * spans left in that pack. objectui#3919 germanised them, so that column is now + * **0** and de's ruling is unanimous rather than 38-to-3. No new `„…“` figure is + * quoted here on purpose: this table was measured *excluding* the two English + * carry-ins the change below removes, so a post-#3919 count taken over the + * present pack is a different unit and the two must not be compared. The table + * stays as measured, being the evidence for the ruling at the time it was taken; + * the live invariant lives in `de-quote-pairing-3876.test.ts`, which now asserts + * that no `de` value holds a U+0022 at all. + * * Related: objectui#3876 / PR #3918 (de quote mispairing — the census this * change moves 45/47/2 to 47/47/0, see `de-quote-pairing-3876.test.ts`), - * objectui#3919 (de `approvalsInbox` ASCII quotes, a separate card and - * deliberately untouched here), objectui#3880, objectui#3844 (the `es` register - * ruling these translations follow: usted). + * objectui#3919 (de `approvalsInbox` ASCII quotes — a separate card, untouched + * by this change and since fixed), objectui#3880, objectui#3844 (the `es` + * register ruling these translations follow: usted). */ import { describe, expect, it } from 'vitest'; From 76f7ed461b02652432f0099b5e41fdb1a5cb4368 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 16:55:58 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(i18n):=20=E4=BF=AE=E6=AD=A3=E6=B8=85?= =?UTF-8?q?=E9=9B=B6=E6=96=AD=E8=A8=80=E6=B3=A8=E9=87=8A=E9=87=8C=E6=8C=87?= =?UTF-8?q?=E9=94=99=E6=96=B9=E5=90=91=E7=9A=84=E4=BA=A4=E5=8F=89=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 反空转守卫的注释写「see the it below」,但钉住 en 用 ASCII 引号的 `leaves en alone` 那条 it 在它上面。改为按名字点明。 --- packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts b/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts index ace9e3fe0e..24b9d6a3c3 100644 --- a/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts +++ b/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts @@ -329,10 +329,10 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight // `toEqual([])` is precisely the assertion shape that also passes when nothing // was produced, so the predicate must be shown to still be able to find - // something. `en` quotes with ASCII by design (see the `it` below), so the - // same filter over `en` returns a long list; a broken `flatten`, an emptied - // pack or a renamed export would take that to zero and fail here instead of - // reading green above. 40 en values at objectui#3919's landing. + // something. `en` quotes with ASCII by design (pinned in the `leaves en alone` + // `it` above), so the same filter over `en` returns a long list; a broken + // `flatten`, an emptied pack or a renamed export would take that to zero and + // fail here instead of reading green above. 40 en values at #3919's landing. expect(DE.length, 'de pack looks empty — the scan would be vacuous').toBeGreaterThan(2000); const enStraight = flatten(builtInLocales.en).filter(([, v]) => v.includes(STRAIGHT)); expect(