fix(i18n): the change card's Confirm button sent text the cloud gate rejects, + parity ratchet - #2905
Merged
Merged
Conversation
…rejects The English `console.ai.changesConfirmMessage` was "Confirm the changes — apply what you just proposed." The cloud confirm gate (service-ai-studio confirm-gate.ts APPROVAL_RE) recognises `apply (this|the) change` — not "apply what". The message therefore failed the gate, and failing it is silent: the agent re-proposes instead of applying and the Confirm button on the change card just looks inert. This hit English conversations AND all eight locales that fall back to English for that key. Now "Confirm — apply the change you just proposed." — singular, so it still matches if the gate ever tightens to a word boundary. The Chinese string always matched (确认修改) and is unchanged. The same literal lives in four places (locale pack, prop default, its doc comment, AiChatPage defaultValue); all four move together. Why the existing guard missed it: i18n.test.ts mirrored only the CHINESE clause of APPROVAL_RE, and the English half was weakened to "starts with Confirm, contains apply" because nothing in this repo could see the real pattern. That assertion passed against a string the gate rejected — green guard, broken feature. The mirror is now verbatim and covers both clauses, driving an it.each over every outbound approval message in zh and en. Two supporting tests keep it honest: the gate must stay narrow (a plain build request is not approval), and planAnswerMessage must NOT match, since it answers a structure question rather than granting blanket approval. Verified by running the real regex against every outbound string, and by mutation-testing the new guard against the exact string that shipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…ently erode #2903 translated `console`, `home`, `topbar` and `layout` into all ten packs. Nothing stopped that from decaying: `fallbackLng: 'en'` means dropping a key from `de` renders English, which reads as "not translated yet" rather than "we lost this", and the missing-key handler is dev-only so CI never sees it. This is #2872's P3 applied only to the namespaces that are actually complete. Full parity would fail today by ~277 keys per pack with no action attached — a broken build, not a guard. Widen RATCHETED_NAMESPACES as each remaining namespace is translated, not before. Asserts both directions, because the packs have drifted both ways before: every ratcheted en key exists in the other nine packs, and no pack defines a ratcheted key en lacks (#2872 part b was exactly that, 74 keys deep, hidden behind a component-private fallback so English "happened to" render). The four outbound agent messages are excluded — they are deliberately absent from the eight non-gate packs, and outbound-agent-messages.test.ts owns that invariant; the two guards would otherwise contradict each other. A non-vacuity assertion pins the ratchet at >300 keys and requires every named namespace to contribute, so a rename cannot reduce the file to a no-op. Mutation-tested in both directions: removing topbar.offline from fr fails the first, adding an en-less key to fr fails the second. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC
Contributor
❌ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
July 28, 2026 07:05
os-zhuang
added a commit
that referenced
this pull request
Jul 28, 2026
…ey parity (#2872) (#2909) Translates the remaining 275 keys x 8 packs = 2,200 strings, closing #2872. Largest namespaces: `grid` (101, mostly the import wizard), `gantt` (58), `dashboard` (25), plus a long tail across list/auth/fields/marketplace/ capability and nine others. Every pack is now at parity with en: 2,495 of 2,499 keys, zero keys en lacks. The four-key remainder is the outbound-message set, absent by design so t() falls through to English and the cloud confirm gate keeps recognising it. P3 is now enforceable. The four-namespace ratchet from #2905 is replaced by all-locales-key-parity.test.ts, asserting: every pack defines every en key; no pack defines a key en lacks (#2872 part b was 74 keys of exactly this); and placeholders match en per string — both `{{count}}` and the single-brace `{count}` form that two gantt.autoScheduleDlg keys use because their call site does a literal .replace('{count}', …) instead of i18next interpolation. All three assertions were mutation-tested, single-brace form included. A bug the test suite could not have caught: the first merge pass produced DUPLICATE keys in four packs. The key list is the union of what is missing across all eight, but the insert ran unconditionally, so packs that already had detail.created/detail.updated got a second copy. Every test still passed — at runtime the later property wins, so the parity check saw a consistent object. tsc caught it as TS1117 during `turbo build`; ESLint does not flag it and a runtime test cannot, since the duplicate is collapsed before JS sees the object. The merge script now filters per pack. Translations are model-generated, and dense domain terminology (Gantt dependency types, the import wizard's upsert/match-field vocabulary) is where that is weakest. Shipping as a reviewable first draft, not a finished localization. Mechanically verified: parity both directions, placeholder shape per string, and that no outbound agent message was translated. Claude-Session: https://claude.ai/code/session_01TubWYdWquVkS9dj733sDmC Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits, both following from getting read access to
objectstack-ai/cloudand being able to seeAPPROVAL_REfor the first time.Closes the residual unknown I flagged in #2900 — by confirming it was a real bug, not a theoretical risk.
1. The Confirm button was sending text the gate rejects
The English
console.ai.changesConfirmMessagewas:service-ai-studio/src/tools/confirm-gate.tsAPPROVAL_RErecognisesapply (this|the) change. Not "apply what". So the message failed the gate — and failing it is silent: the agent re-proposes instead of applying, and the Confirm button on the change card simply looks inert. Nothing errors, nothing logs.This hit English conversations and all eight locales that fall back to English for that key.
Verified by executing the real regex against every outbound string:
planApproveMessageplanApproveDefaultsMessagechangesConfirmMessageplanAnswerMessageFor "…", go with: …Now
Confirm — apply the change you just proposed.— singular, so it still matches if the gate ever tightenschangetochange\b. The Chinese string always matched (确认修改) and is unchanged. The same literal lives in four places (locale pack,ChatbotEnhancedprop default, its doc comment,AiChatPagedefaultValue); all four move together.Why the existing guard was green while the feature was broken
i18n.test.tsmirrored only the Chinese clause. I wrote the English half as "starts withConfirm, containsapply" precisely because nothing in this repo could see the real pattern, and said so in the #2900 body. That assertion passed against a string the gate rejects.Worth stating plainly: a guard written around the limits of what I could verify gave the appearance of coverage over the one thing I couldn't check. Weakening an assertion to fit the available evidence is not the same as testing the contract — and in a green CI run the two are indistinguishable.
The mirror is now verbatim, both clauses, driving an
it.eachover every outbound approval message inzhanden, plus two tests that keep it from rotting into a rubber stamp:帮我搭建一个 CRM/Please build me a CRM app/我不确定这样对不对must NOT read as approval;planAnswerMessagemust NOT match — it answers a structure question; reading as blanket approval would be its own bug.The mirror duplicates a pattern across a repo boundary out of necessity (objectui cannot import from cloud). The comment says so, so whoever next edits
APPROVAL_REknows this copy exists.2. Parity ratchet for the namespaces #2903 backfilled
Nothing stopped that backfill from decaying:
fallbackLng: 'en'means dropping a key fromderenders English, which reads as "not translated yet" rather than "we lost this", and the missing-key handler is dev-only.This is #2872's P3 scoped to the namespaces that are actually complete. Full parity would fail today by ~277 keys per pack with no action attached — a broken build, not a guard.
RATCHETED_NAMESPACESwidens as each remaining namespace lands.Asserts both directions, because the packs have drifted both ways: every ratcheted
enkey exists in the other nine, and no pack defines a ratcheted keyenlacks — #2872 part (b) was exactly that failure, 74 keys deep, hidden behind a component-private fallback so English "happened to" render.The four outbound messages are excluded;
outbound-agent-messages.test.tsowns that invariant and the two guards would otherwise contradict each other. A non-vacuity assertion pins the ratchet at >300 keys and requires every named namespace to contribute.Verification
en.tsfails it withen console.ai.changesConfirmMessage does not match the cloud APPROVAL_RE.topbar.offlinefromfrfails one assertion; adding anen-less key tofrfails the other. My first attempt at this mutation was vacuous — wrong indent, so theperlsilently matched nothing and the suite stayed green. Redone with the removal verified inside the same command.turbo build: 44/44.