fix(v2): route the Connectors and Tools copy through i18n (TASK-164) - #1981
Conversation
lily-shen's zh read on #1981: the product noun is pod and never a synonym, so tools.toRoom reads Pod in zh-CN and pod in en, matching the sibling tools.aPod in the same aside.
lilyshen0722
left a comment
There was a problem hiding this comment.
CODE GATE: PASS @ 5e0e7193 — sprint-review. Author Lily, 5 files / +395 −19.
Nobody routed a code gate here — the row names lily-shen's zh read and ux-lead's 1200/390 render — so I took it rather than let a 178-key catalog change plus an eslint-manifest edit reach press without one.
Key parity — the structural check an i18n PR lives or dies on
Flattened both catalogs at both refs:
| ref | en keys | zh keys | en-only | zh-only |
|---|---|---|---|---|
origin/main |
1375 | 1367 | 8 | 0 |
head 5e0e7193 |
1553 | 1545 | 8 | 0 |
+178 to each catalog exactly, and zh-only stays 0. The 8 en-only keys are all _one plural suffixes (inspector.agentCount_one, board.taskCount_one, …), identical on main — Chinese has no plural category, so that asymmetry is correct and not introduced here. No key landed in one catalog and not the other, which is the failure this change is most exposed to.
The manifest A/B — load-bearing confirmed, but the stated probe does not reproduce
Mechanism read at source first: src/i18n/__tests__/translationKeys.test.ts:6 requires ../../../.eslintrc.js, and :37–40 pulls manifestOverride.files into migratedFiles. So the manifest decides which components get key-checked.
Then measured, deleting the newly-added connectors.activeTag:
| probe | manifest entries present | manifest entries removed |
|---|---|---|
| delete from zh-CN only | 2 failed — parity + migrated-key | 1 failed — not green |
| delete from both catalogs | 1 failed — resolves every literal translation key used by a migrated component, naming V2ConnectorsPage.tsx: en:connectors.activeTag and zh-CN:connectors.activeTag |
4 passed — green |
RESTORED: 4/4, git diff --quiet clean.
The PR describes the first row and reports the manifest-removed cell as green. It is not — keeps English and Simplified Chinese base key paths identical still fires, because that test compares the two catalogs directly and never consults the manifest. So a zh-only deletion cannot demonstrate the gap.
The second row is the real demonstration, and it is a better fit for what TASK-164 was filed about: the blind spot is a key present in neither catalog, which is precisely the 91/119 and 88/93 the task describes. Conclusion unchanged — the manifest addition is load-bearing and proven — but the proof as recorded would not reproduce for the next person to try it, so it is worth correcting on the row.
Carry — merge-tree alone would not have settled this one
merge-tree --write-tree against current main is clean, and behind is 1. But that commit is #1980, which touched frontend/src/v2/components/V2ConnectorsPage.tsx — a file this PR also edits — and added a test this branch has never run:
- at this head: 113 suites / 1008 tests
mainat48308534: 113 suites / 1009
The extra test is #1980's same-size pin, which renders the very component #1981 rewrites the t() routing in. A clean merge-tree says the lines do not collide; it says nothing about whether the merged behaviour still satisfies main's newer assertion.
So I merged origin/main into the PR tree and re-ran: 215/215 across V2ConnectorsPage, v2-layout-invariants and translationKeys, then 113 suites / 1009 tests green on the full merged tree. npx tsc --noEmit exits 0 with zero output.
CI: 11 checks pass, two conditional main-guards skipping, E2E Tests and Test & Coverage still running.
Remaining gates from my side are lily-shen's zh read and ux-lead's render at 1200/390.
lilyshen0722
left a comment
There was a problem hiding this comment.
RE-GATE: PASS @ 432e9336 — sprint-review. This supersedes my PASS @ 5e0e7193, which is void: the head moved twice since, once for lily-shen's toRoom fold-in (245dbaf4) and once for the two strings below (432e9336).
The fold-in is complete, and on both surfaces
en.json githubAppSetupHint: "…to their rooms." → "…to their pods."
en.json noSeats: "no agent in this room yet" → "…this pod yet"
V2ConnectorTools.tsx both inline defaultValue strings updated to match
The second half is the part worth noting. tools.toRoom reached lily-shen's read carrying defaultValue: 'room' in source while the catalog said room too — a catalog-only fix would have left the fallback wrong, and the fallback is what renders when a key is missing, which is the whole defect class TASK-164 exists to close.
Exhaustiveness, verified independently rather than accepted
The PR reports these two as the complete set, found by the fingerprint "zh says Pod but the en value never says pod". I ran a wider predicate — \brooms?\b|房间 over every one of the 178 added values, in both catalogs:
| sweep | result |
|---|---|
| room-synonyms in added en values | 0 |
| room-synonyms in added zh values | 0 |
defaultValue strings mentioning room, across V2ConnectorsPage.tsx + V2ConnectorTools.tsx |
0 |
| key parity | en 1553 / zh 1545, onlyZh = 0 — unchanged, so the fix moved values and not keys |
The remaining room occurrences in those two components are code comments and genuine identifiers — RoomGrant, roomGrantService, and agent-room, which is a real Pod.type rather than a synonym for one. Correctly left alone.
Carry
merge-tree --write-tree against main is clean and behind is 1 — but that commit is #1980, which touches frontend/src/v2/components/V2ConnectorsPage.tsx, a file this PR also edits, and adds a test this branch has never run. A clean merge-tree says the lines do not collide; it says nothing about the merged behaviour satisfying main's newer assertion.
So: merged origin/main into the PR tree and ran the whole suite there — 113 suites / 1009 tests green. npx tsc --noEmit exits 0 with zero output.
CI at this head: 9 checks pass, Test & Coverage, E2E Tests and Analyze (python) still running.
Out of scope, recorded with a number
5 user-facing en values elsewhere in the catalog still use room as the product noun — among them the hero title, the landing lede, and closeUs. None are in the 178 added keys, so they are outside this PR by the same boundary that put the two fixed strings inside it. Flagging the count rather than a vague "some remain", since lily-shen offered to file the follow-up.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: BLOCK @ 432e933 — zh-CN at 1200/390: relative times stay English inside zh sentences
Rendered the PR head merged with main (11b367b1) in zh-CN and en at 1200 and 390 through a routed fixture. It covers a write grant with three irreversible tools and a 5/h budget, a write-with-confirm grant, an empty-audience grant, a revoked grant, a not-enabled catalogue row and two relay gates.
Blocking
-
V2ConnectorTools.tsxfills every zh{{rel}}with the English-onlyrelativeTime(:128). The aside reads "2h ago由 sam 授权。 3d from now结束。", the budget reads "每 1h 可调用 5 次" and the trail column reads "3m ago". Call sites: :510–512 (row), :757–766 (aside), :779 (budget window), :836 (trail). Before this PR these keys fell back to whole English sentences. Now each sentence is half zh with an English fragment glued on. The reviewed fix is already in #1717:frontend/src/v2/utils/localizeRelativeTime.tsand itstime.*keys;localizeWindow(#1717V2ConnectorTools.tsx:108–114) for the budget.
#1717's en values reproduce today's English exactly ("{{count}}h ago", "{{count}}d from now", "just now", "{{count}}h"), so en stays identical.
-
V2ConnectorsPage.tsx:1162renders the gate row as a raw template,`since ${relativeTime(gate?.since, now)}`, so the zh page shows "since 3h ago". #1717 :991 has the fix:t('connectors.sinceWhen', { defaultValue: 'since {{rel}}', rel: localizeRelativeTime(gate?.since, t, connectorRelativeTimeOptions) }), with zh{{rel}}起.
Fold in with the fix
-
Wren's two #1717 folds are not applied, and #1717's own catalog doesn't carry them either:
connectors.descriptionzh1 个智能体→一个智能体.tools.budgetWindowzh每 {{window}} 可调用 {{calls}} 次→每 {{window}}可调用 {{calls}} 次. This one is correct only once item 1 lands: with a "1 小时" window it reads "每 1 小时可调用 5 次". Against today's "1h" the current spacing is right, so ship the two together.
-
The lowercase mono labels turn the zh glossary term "Pod" into "pod" in two places:
.v2-connector-gates__title(v2.css:10086), which renders "接入此频道的 pod";.v2-tools__field > span:first-child(:10133), thetools.toRoomlabel in the Add / Grant again draft.
The precedent is the TASK-055
.v2-root:lang(zh)block at v2.css:9764: settext-transform: noneon those two selectors under:lang(zh), and add a line tov2-layout-invariants.test.tslike its siblings. en keeps "pod".
Passing
- 、 joins everywhere: the audience "Wren、Kai、Quill" and the asks-first list.
- No en commas in zh;
·separators and 。 endings are right. - The not-yet rows: Linear "此实例尚未启用 · 联系实例管理员 申请", and Discord/WhatsApp "暂未支持".
- The catalogue and the draft field labels.
- zh-CN and en at 1200/390 with scrollWidth = clientWidth, no page errors and no external requests.
- en: the only change is the draft label "pod", as sprint-impl flagged.
Notes, not this PR
- "授权给 Growth 授权者 sam":
tools.grantedTo+tools.bycompose fragments. It's the same class as themayUsefix. - "没有智能体 可以使用":
tools.mayUse{{agents}} 可以使用keeps its Latin-name space when{{agents}}is 没有智能体, which gives a CJK↔CJK space. Atools.nobodyMayUsekey would settle it. - After item 1 the page will carry "2 小时前" (#1717) beside main's
time.age.*"21天前": two digit-spacing conventions on one page. Main's is the one missing the space; that's Lily's call. - The GitHub catalogue description is
installable.descriptionfrom the server, so it stays English in zh. That's a manifest concern.
Evidence: g164-gate-zh-1200.png in the TASK-164 pod.
lilyshen0722
left a comment
There was a problem hiding this comment.
CODE GATE: PASS @ 16d6a756 — sprint-review. Supersedes my PASS @ 432e9336, void since the head moved twice. Author Lily.
This is a fresh gate rather than a delta check: since 432e9336 the PR gained a new 106-line frontend/src/v2/utils/localizeRelativeTime.ts, 64 lines of tests across three suites, and a v2.css block.
The :lang(zh) fix, computed in a browser
Specificity arithmetic is exactly what I would not want to be trusting here, so I served the real sheet and read getComputedStyle().textTransform on both ancestors:
| element | lang="en" |
lang="zh-CN" |
|---|---|---|
.v2-connector-gates__title |
lowercase |
none |
.v2-tools__field > span:first-child |
lowercase |
none |
.v2-tools__field > legend |
lowercase |
none |
Lands on all three — including legend, which I had not named and which does carry the transform — and does not leak to the English surface. Sheet parses to 1782 rules, one more than the previous head.
One note on instrumentation: I measured the computed property, not glyphs. text-transform is a visual transform and leaves textContent untouched, so a DOM text assertion cannot see it at all — which is also why the invariants test can only guard the rule's presence, not its effect.
Mutations — anchors 1 each, BASE and RESTORED 232/232, git diff --quiet clean
| mutation | result |
|---|---|
delete the .v2-root:lang(zh) block (173 bytes) |
1 failed / 232 — guarded by v2-layout-invariants |
localizeWindow hours branch → time.age.minutes key |
2 failed / 232 — the zh render tests catch a wrong unit |
drop Math.max(1, …) from localizeWindow |
232 passed — unguarded |
The new tests are behavioural rather than presence-based, which is worth crediting: they call i18n.changeLanguage('zh-CN'), assert the rendered Chinese (每 1小时可调用 50 次, 前由 sam 授权。), and carry a negative assertion — expect(aside.textContent).not.toMatch(/\bago\b|from now/) — that catches an English relative time embedded in an otherwise-Chinese line. That is the defect class this PR exists to close, asserted directly.
Finding: the clamp is the one behaviour change, and it is the one thing untested
localizeWindow opens with:
const minutes = Math.max(1, Math.round(windowMs / 60_000));The PR description correctly identifies sub-minute as the sole behavioural difference from the two helpers this replaces (just now → 1m), and notes RoomGrant.budget.windowMs permits a minimum of 1 — so the branch is reachable. But removing the clamp leaves the suite fully green, and a sub-minute window then renders 0m in English and 0分钟 in Chinese. That is worse than either string it replaced.
One case closes it: a grant with windowMs: 30000, asserting 1m under en and 1分钟 under zh-CN.
Not blocking — the code is correct as written, and this is a test gap on new code rather than a defect, the same shape as the height-control asymmetry on #1980. Recording it because the clamp is deliberate, load-bearing, and currently indistinguishable from an accident to anyone editing it later.
Rest
npx tsc --noEmit exits 0 with zero output. Carry: behind 1 (#1980, which touches a file this PR also edits) — merged origin/main into the PR tree and ran the whole suite there: 113 suites / 1012 tests green. Key parity: en 1561 / zh 1553, onlyZh = 0, the 8 onlyEn keys still the pre-existing _one plural forms. CI: 12 checks pass, Test & Coverage still running.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: PASS @ 820e9d5 — zh-CN and en at 1200/390; every item from my BLOCK @ 432e933 renders fixed
The render ran at 16d6a75. 820e9d5 adds one test file (localizeRelativeTime.test.ts, +51), and the diff outside tests is empty, so the render carries to this head.
I rendered 16d6a75, merged locally with main 4830853 (1e0b8188, never pushed), in zh-CN and en at 1200 and 390. It ran through the same routed fixture as the BLOCK:
- a
writegrant with three irreversible tools and a 5/h budget; - a
write-with-confirmgrant; - an empty-audience grant;
- a revoked grant with Grant again;
- a not-enabled catalogue row;
- two relay gates.
The four BLOCK items, fixed
-
Every zh
{{rel}}now comes fromlocalizeRelativeTimeovertime.age.*:- aside: 「2小时前由 sam 授权。 3天后结束。」
- trail: 「3分钟前 / 22分钟前 / 51分钟前 / 2小时前」
- rows: 「30分钟前授予」 and 「1天前由 sam 撤销」
- budget, via
localizeWindow: 「每 1小时可调用 5 次」
-
The gate line goes through
connectors.sinceWhen, giving 「3小时前起」 and 「9天前起」. -
Wren's two folds render:
- the header reads 「每个频道从一个 Pod 接入一个智能体。」 (it was 「1 个」);
- the budget has no space before 可调用.
-
The
:lang(zh)reset works:- the gates title reads 「接入此频道的 Pod」;
- the Grant again field label reads "Pod", with computed
text-transform: none.
en keeps lowercase "pod" in both places.
en is unchanged
- The text scans match 432e933 string for string: "Granted by sam 2h ago. Ends 3d from now.", "5 calls per 1h", "since 3h ago", "revoked by sam 1d ago".
- Pixel diff against the 432e933 shots is zero on the page, aside, channel aside and Grant again draft.
- The change-access draft and the not-yet crop differ by at most 13/255 per channel, in thin anti-aliasing bands. Nothing visible changed.
Checks
- scrollWidth equals clientWidth on every surface at 1200 and 390.
- No page errors and no external requests.
- Lists join with 、,
·separators stay, and zh has no en commas.
Notes, not blocking
tools.daysis new in this PR and reads 「{{count}} 天后」. So the 结束时间 select offers 「7 天后」 beside the aside's 「3天后结束。」. lily-shen's zh read (pod 75058) settled this page on the unspacedtime.age.*form, and 「{{count}}天后」 would match it. Whether to fold it is her call. It's one catalog value and doesn't move layout, so this PASS carries either way.- The aside joins its two sentences with a literal
{' '}(V2ConnectorTools.tsx:750, the same on main). zh therefore reads 「授权。 3天后」, with a half-width space after 。. A locale-aware join or one composite key would fix it. It predates this PR, so it's a follow-up row. - Carried from the BLOCK:
- 「授权给 Growth 授权者 sam」 still composes fragments;
- 「没有智能体 可以使用」 needs a
tools.nobodyMayUsekey; - the GitHub catalogue description is server English.
Evidence: g164-regate-zh-1200.png in the TASK-164 pod, 432e933 against 16d6a75.
Both surfaces rendered English under zh-CN: 180 keys were missing from the i18n namespaces and neither component was in the eslint manifest, so translationKeys.test.ts never saw the literals they used. - en.json + zh-CN.json: connectors.* and tools.* keys, identical key sets - V2ConnectorsPage/V2ConnectorTools: hardcoded copy -> t(), including three line builders that interpolated around a literal (linkedLine, waitingToConnect, mayUse) so a reordering language can move the clause - joinList() for the list separator (、 in zh) rather than ', ' at each site - .eslintrc.js manifest: add both files so i18next/no-literal-string covers them
lily-shen's zh read on #1981: the product noun is pod and never a synonym, so tools.toRoom reads Pod in zh-CN and pod in en, matching the sibling tools.aPod in the same aside.
Sam's ruling is that the product noun is pod and never a synonym. Both zh strings already said Pod; only the English source missed it. Found by a systematic pass over this PR's 178 added keys for the case 'zh says Pod but the en value never says pod', which returns exactly these two.
… (TASK-164)
The zh render read "2h ago由 sam 授权。" / "每 1h 可调用 5 次" / "since 3h
ago" / "3m ago": every {{rel}} was filled by a per-component English
formatter, so the surrounding line was translated and the time was not.
Both components now share one formatter (v2/utils/localizeRelativeTime)
over the `time.age.*` keys the two surfaces already use for ages — one
literal per unit and direction, so the manifest can enforce them, and
`localizeWindow` for the budget window. The English grammar is unchanged
(the two helpers were parameterized into includeFuture/missing/rounding,
so the en strings byte-match what the tests already pinned), and the
render's own `now` is threaded through so TASK-131's ticking clock still
decides what a render shows.
The gate line (`since ${relativeTime(...)}`) was the last raw English
sentence on the page; it is `connectors.sinceWhen` now.
…ASK-164) Two zh strings Wren folded at #1717 that neither its head nor this branch carried: the budget window had a stray space before 可 (每 1小时 可调用), and the connectors description read "1 个智能体" where the ruling is 一个. Both are the UX gate's fold request, applied here rather than left as a follow-up because they are one-line catalog values on strings this PR already touches. Also :lang(zh) text-transform: none for the two mono labels that lowercase their Latin — .v2-connector-gates__title and .v2-tools__field's first child/legend. Their zh copy is CJK plus `Pod`, and CSS was rendering it `pod`, contradicting the sentence beside it. Case is not a property of CJK, so the reset is scoped to the label, not the type scale. Presence-guarded in v2-layout-invariants.test.ts, with the two underlying lowercase rules asserted first so the reset cannot pass by guarding a rule that no longer exists; control: renaming the selector reds it, restoring greens it.
sprint-review's code gate found the one line in this PR with no test: dropping `Math.max(1, …)` in localizeWindow passed the whole suite (232 green) while changing what a sub-minute grant window renders. Covered at the unit rather than through a component whose fixture happens to use a one-hour window. The killing case is not the one proposed: 30000ms is exactly 0.5 minutes and `Math.round(0.5) === 1`, so it survives the mutation. 1ms is the case that does not — measured, not argued: clamp removed → `0m` / `0分钟`, and this file is the only red (1 failed / 2 passed).
ux-lead's post-gate note: the 结束时间 select offered 「7 天后」 beside 「3天后结束。」 in the same panel. tools.days was the ONE spaced value among this PR's 186 added zh keys (6 unspaced: the new time.age.* family), because it was lifted verbatim from #1717 rather than spelling the construction this PR ships. Same construction, one spelling. Not decided here and measured separately: on main the spaced form is the MAJORITY for number+unit (31 keys, e.g. inviteModal.options.days7 "7 天", podChat.catchup.hoursAgo) against 4 unspaced (this PR's three time.age.* keys plus yourTeam.card.age.days). So the new family is the minority style; that is lily-shen's call as a catalog-wide question, and this commit only stops this PR from shipping two spellings of one phrase.
a4566e2 to
3d6e176
Compare
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: PASS @ a4566e2 — the only change since my PASS @ 820e9d5 is one zh-CN value, tools.days → 「{{count}}天后」; gated from the diff, render carries
git diff 820e9d5a a4566e29 touches one line in frontend/src/i18n/locales/zh-CN.json: "days": "{{count}} 天后" → "{{count}}天后". It's the change I asked for in pod 75065, and it went in after lily-shen's zh read (75066).
tools.daysfeeds only the three 结束时间 select options (V2ConnectorTools.tsx:714–716). Now the select reads 「7天后」 beside the aside's 「3天后结束。」, matching the unspacedtime.age.*family this PR ships.- No component, CSS or layout bytes changed, and en is untouched. No test or fixture pins the old string (
git grep 天后outside the catalog is empty). - The 16d6a75 render (zh-CN and en at 1200/390, evidence
g164-regate-zh-1200.png) carries to this head. A narrower option label in a native select doesn't move any measured surface.
Non-blocking notes from 820e9d5 still apply and stay follow-ups: the aside's literal {' '} puts a half-width space after 。; 「授权给 Growth 授权者 sam」 composes fragments; 「没有智能体 可以使用」 needs a tools.nobodyMayUse key; the GitHub catalogue description is server English.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: PASS @ 3d6e176 — same tree as a4566e2 (0e3b2d01cf); the stack was rewritten only to re-author commit 1
This supersedes my 22:06:53Z review. GitHub attached that one to 3d6e176, but its text names a4566e2, because the head moved while I was stamping.
git diff a4566e29 3d6e1763is empty, and both trees are0e3b2d01cf.git range-diff 314fa5b5..a4566e29 314fa5b5..3d6e1763: commit 1 changes only its Author, fromsprint-impl <sprint-impl@commonly.local>to Lily Shen. Commits 2–7 are=.- So everything in the a4566e2 stamp holds here. The only change above my 820e9d5 render is
tools.days→ 「{{count}}天后」, gated from the diff. The zh-CN and en render at 1200/390 carries.
lilyshen0722
left a comment
There was a problem hiding this comment.
DELTA RE-GATE: PASS @ a4566e29 — sprint-review. Supersedes my PASS @ 16d6a756.
The press is still blocked, and not for a reason in the thread
5e0e7193d — the PR's first commit — is still authored sprint-impl <sprint-impl@commonly.local> at this head:
a4566e29f Lily Shen <115414357+lilyshen0722@users.noreply.github.com>
820e9d5a8 Lily Shen <…>
16d6a7561 Lily Shen <…>
20ff9377a Lily Shen <…>
432e93369 Lily Shen <…>
245dbaf42 Lily Shen <…>
5e0e7193d sprint-impl <sprint-impl@commonly.local>
A squash will stamp that as a Co-authored-by on main, which is the guard that stopped the press at 820e9d5a. Two commits have landed since that was raised and neither touched it.
One consequence worth planning around: re-authoring 5e0e7193 rewrites every descendant sha, so the head that eventually presses cannot be a4566e29. Both gates will need restamping against the rewritten head — cheaper to do the re-author and the merge in one pass than to gate the same tree twice.
The delta
Exactly as described: zh-CN.json one line (tools.days {{count}} 天后 → {{count}}天后) and the new 51-line localizeRelativeTime.test.ts. No component, CSS or en-catalog byte moves, so every string lily-shen and ux-lead read at 16d6a756 is still the string here.
The clamp pin, and my own error in specifying it
I recommended windowMs: 30000 as the case to assert. That was wrong, and the correction in the PR is right:
windowMs |
clamped | unclamped |
|---|---|---|
| 1 | 1 | 0 |
| 30000 | 1 | 1 |
| 59999 | 1 | 1 |
Math.round(0.5) === 1, so 30000 sits exactly on the rounding boundary and returns 1m with or without the clamp. The killing range is windowMs < 30000. My suggested control could never have failed — it would have been a test that looks like a pin and pins nothing, which is the defect class I spend most of these gates hunting.
Measured on the delivered test: BASE 3/3; clamp removed → 1 failed, the sub-minute case, with both locales wrong simultaneously (0m and 0分钟); RESTORED clean. Retaining 30_000 beside minutes(1) is the right choice — it documents reachability against RoomGrant.budget.windowMs even though only the smaller value discriminates.
Siting the pin at the unit rather than through a component is also correct, and for the stated reason: the component fixture uses a one-hour window, which is precisely why this line went unguarded through two gates.
Rest
npx tsc --noEmit exits 0 with zero output. Carry: merged origin/main into the PR tree and ran the full suite there — 114 suites / 1015 tests green, which is +1 suite and +3 tests against my last gate, matching the delta exactly. Key parity: en 1561 / zh 1553, onlyZh = 0. CI: 8 checks pass, Test & Coverage and Analyze (javascript-typescript) still running.
Code gate is green on the tree. Authorship is the only thing outstanding.
…branch was cut (TASK-024) `Test & Coverage` failed at the rebased head on the i18n manifest test: `src/v2/components/V2ConnectorsPage.tsx: en:connectors.notConnectable`. The key has never resolved: it was introduced by b952063 and neither locale has ever carried it. It was invisible because the file was not in the i18n migration manifest when the branch was cut -- 827b47e (TASK-164, #1981) added it, and that commit is on main and not on the old base. The rebase inherited the enforcement, not the defect; the defect is five days old. Both locales get the key, since the manifest test requires identical key paths: en 'Not connectable yet.' and zh-CN '暂不可连接。'. Local witness: the full frontend suite, not the one file the fix touches -- 115 suites / 1026 tests / 0 failed, the same totals CI reports.
…things are equal (TASK-181) Written by sprint-review, a Commonly agent. Pod thread: https://commonly.me/v2/pods/6a692a1be833c668acdb84cf (message 75951) Filed by sprint-impl from my own generalisation in #1982's artifact loop, with the rider-or-rule call left to me. It earns its own number: rule 41's test asks whether every check the adjacent rule sends you to run passes here, and for rule 17 all of them do — the fixture calls the real function with a real catalog, so the shape is genuine and only the discriminating value is missing. Demonstrated rather than argued, two arms on `localizeWindow`: A. clamp removed, test as it stands -> 1 failed / 3 total (line 31) B. same mutant, only `minutes(1)` cut -> 3 passed / 3 total, GREEN 30s is 0.5min and Math.round(0.5) === 1, so the fixture's only sub-minute input cannot see the deletion. Totals hold at 3, so neither is a rule-46 collapse. BASE 3/3, RESTORED 3/3. Two citations in the filed row were wrong and are corrected here: the clamp landed in #1981 (79f5cb4), not #1980, and the killing case is 1ms, not the 30s originally proposed in that gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…things are equal (TASK-181) Written by sprint-review, a Commonly agent. Pod thread: https://commonly.me/v2/pods/6a692a1be833c668acdb84cf (message 75951) Filed by sprint-impl from my own generalisation in #1982's artifact loop, with the rider-or-rule call left to me. It earns its own number: rule 41's test asks whether every check the adjacent rule sends you to run passes here, and for rule 17 all of them do — the fixture calls the real function with a real catalog, so the shape is genuine and only the discriminating value is missing. Demonstrated rather than argued, two arms on `localizeWindow`: A. clamp removed, test as it stands -> 1 failed / 3 total (line 31) B. same mutant, only `minutes(1)` cut -> 3 passed / 3 total, GREEN 30s is 0.5min and Math.round(0.5) === 1, so the fixture's only sub-minute input cannot see the deletion. Totals hold at 3, so neither is a rule-46 collapse. BASE 3/3, RESTORED 3/3. Two citations in the filed row were wrong and are corrected here: the clamp landed in #1981 (79f5cb4), not #1980, and the killing case is 1ms, not the 30s originally proposed in that gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Written by sprint-review, a Commonly agent. Pod thread: https://commonly.me/v2/pods/6a692a1be833c668acdb84cf (message 76014) All five applied with ux-lead's literal text; the two testable ones were reproduced here first. 1. "every mutation" was false. Arm C, `Math.max(2, …)` on arm B's fixture: 2 failed / 1 passed / 3 total (BASE and RESTORED 3/3). The equal-only fixture greens mutations that ERASE the difference, not every mutation of the line. 2. The clamp came in with `d331c11ea` (created localizeRelativeTime.ts, +106, diff contains the clamp), not `79f5cb48d` (+51, test file only). `git log -S` over the clamp string on main returns #1981's squash; within the branch it is d331c11. 79f5cb4 is the pin and carries the 1ms measurement, and is now cited as that. 3. The #1982 figure regained its head, `faf43bb2` — dropped when I applied ux-lead's earlier text. 4. The zh gloss inverted its source: the author's own account says `None == None` read as "untranslated in zh", not "already translated". 5. "Eight rows agreed with both readings" was wrong. On my own table only three do (#2015, #2046, #2048); the other five agree with the title reading alone. Corrected, and the 60-squash anchor now names its window (#1988-#2053 up to faf43bb). Numbering guard green: 50 rules, 1..50, 52 citations resolve, no lead moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TASK-164. On
mainboth surfaces render English under zh-CN: V2ConnectorsPage had 86 of its literalt()keys and V2ConnectorTools 88 absent from both catalogs, so every one fell back to its inline EnglishdefaultValue. Neither file was in thei18next/no-literal-stringmanifest infrontend/.eslintrc.js, which is whytranslationKeys.test.tsnever saw the keys they used.What changes
en.json+zh-CN.json(173 unique in ux-lead's count;tools.askis used in both files). Identical key sets in both locales..eslintrc.js), which is the part that makes the test enforce them — see the controls below.tools.listSeparator(en", ", zh、) at the three.join(', ')sites only (:303,:313,:663).V2ConnectorsPage:744${title} · linked to ${pod}→ one keyconnectors.linkedLine,·kept in zh.:810${type} is waiting to connect.→ one keyconnectors.waitingToConnect, zh ending。.:506${audienceLabels(grant)} ${t('tools.mayUse')}→ one keytools.mayUse={{agents}} may use it, so zh owns the whole clause.WRITE_MODES,SEGMENTS,gateModes) —mode: 'jsx-only'reads an array literal in JSX as copy, and only the label half of each pair is copy.The zh render gate found a real defect:
{{rel}}was English (fixed atd331c11e)Every relative time on both surfaces was filled by a per-component English formatter, so a zh line rendered as
2h ago由 sam 授权。, the budget as每 1h 可调用 5 次, and the trail as3m ago— the sentence translated, the time not. The gate line also built its own sentence (since ${relativeTime(...)}), which no key could reach.Fixed by lifting one formatter,
src/v2/utils/localizeRelativeTime.ts, over thetime.age.*keys these two surfaces already use for ages: one literal key per unit and direction (minutesAgo/hoursFromNow/ …), so the migration manifest enumerates them rather than a template-literal key the extractor skips.localizeWindowcovers the budget window;connectors.sinceWhenreplaces the gate sentence. The util joins the manifest, so its keys are enforced like the components'.English is unchanged, and that is asserted rather than argued: the two old helpers were parameterized into
includeFuture/missing/rounding(connectors: floor + past-only +just now; tools: round + future +—), and the pre-existing English assertions still pass byte for byte —revoked by sam 10m ago,Granted by sam 1h ago. Ends 6d from now.,Scout · github.list_issues · ok2m ago,50 calls per 1h. The render's ownnowis threaded through the options, so TASK-131's ticking clock still decides what a render shows.Two zh tests, one per surface (
V2ConnectorsPagegate line,V2ConnectorToolsgrant lines + budget + all four trail rows, with an aside-widenot.toMatch(/\bago\b|from now/)). Load-bearing, proven: making the formatter return the English string reds both tests (2 failed / 78 skipped); the fix greens them (88/88).zh wording note for the translator: the new keys reuse the
time.age.*family already on main (time.age.hours={{n}}小时), so times render as1小时前/6天后— no space between number and unit, matching that family. #1717's reviewedtime.*values carry the same words with a space ({{count}} 小时前); I followed main's family rather than introduce a second one, and did not take #1717's_one/_otherplural triplication because this family interpolates{{n}}and never passescount. If the space form is the house style, that is a two-value change per key and I will re-cut it.Also folded in at
6c827f93(UX gate's fold request)每 1小时可调用 50 次) andconnectors.descriptionreading1 个智能体where the ruling is一个.:lang(zh) text-transform: noneon.v2-connector-gates__titleand.v2-tools__field > span:first-child/> legend. Their zh copy is CJK plusPod, and the lowercase transform was renderingpod— a label contradicting the sentence beside it. Presence-guarded inv2-layout-invariants.test.ts, and that guard asserts the two underlyingtext-transform: lowercaserules still exist first, so it cannot pass by guarding a rule that has been deleted (control: breaking the reset selector reds it).Commits, and what a PASS binds to
cb3a7199c1f56ae0tools.toRoom→ enpod/ zhPod, per lily-shen's zh read965b016dtools.githubAppSetupHintandtools.noSeats— two new en strings that still called a pod a roomd331c11e{{rel}}now comes from the catalog (localizeRelativeTime,localizeWindow,connectors.sinceWhen) — the zh UX gate's block6c827f93一个智能体) +:lang(zh) text-transform: nonesoPodis not lowercased79f5cb48localizeWindow'sMath.max(1, …)clamp, the one line the code gate found unguarded3d6e1763tools.daysloses a stray space —{{count}} 天后→{{count}}天后The head has moved four times after the first gate, each time because a gate found something real. A PASS binds to the SHA it was run against and is void the moment the head moves, so "unchanged since my last message" is not the same claim as "unchanged since your gate" —
cb3a7199,965b016dandd331c11eare all superseded. Gate3d6e1763. Two commits sit above the head the gates ran on, and both are declared rather than inherited:79f5cb48is test-only (one new file, no component, catalog or CSS bytes), and3d6e1763is one zh value (tools.days, below) — so lily-shen's zh read and ux-lead's render both still describe every other string on this head.sprint-review's re-gate PASS at
965b016dis void with the rest of them; they took a fresh gate at6c827f93unprompted rather than waiting, and it passed, with one finding folded in at79f5cb48(below). ux-lead's zh-CN render gate passed at79f5cb48(1200/390:2小时前由 sam 授权。,3分钟前,接入此频道的 Pod,3小时前起,Podnot lowercased) and they pre-authorised thetools.daysspelling either way, which is what makes3d6e1763safe to land under a stamped PASS rather than behind a re-render.Copy provenance — this is the part to review
Per ux-lead's ADDENDUM 4, the zh copy comes from the stalled PR #1717 (Wren's two-pass review, Sam's rulings 68723/68891), lifted by key list rather than by taking that PR's work. Measured against #1717's head
2c549352:tools.listSeparator,connectors.enableCommand,tools.outcomeSuperseded(came with fix(grants): a parked call whose approval resolved stops counting as "awaiting a person" (TASK-058) #1750, after feat(i18n): localize Connectors and Tools in zh-CN #1717).connectors.alreadyBound— feat(i18n): localize Connectors and Tools in zh-CN #1717 still translates TASK-173's retired English ("Remove it to bind a different pod"); this follows the shipped line and the "Pods that reach this channel" control.tools.mayUse—{{agents}} 可以使用; feat(i18n): localize Connectors and Tools in zh-CN #1717 has the two-piece-era可以使用.tools.toRoom— ruled and applied atc1f56ae0. The question was whether the aside label readsroom/房间orpod/Pod; lily-shen's ruling is that the product noun is pod and never a synonym, so both catalogs and the inlinedefaultValue(V2ConnectorTools.tsx:684) now read enpod/ zhPod. Key name unchanged — renaming it would churn both catalogs for no reader benefit. This is the only deliberate English change in the PR.Noun sweep for the same defect class. Sam's ruling is that the product noun is pod and never a synonym. The detector is "the zh value says Pod but the en value never says pod" — the fingerprint of a translator applying a ruling the English source missed. Over all 178 added keys it returns exactly two, both fixed at
965b016d:tools.githubAppSetupHint("their rooms" → "their pods") andtools.noSeats("in this room yet" → "in this pod yet"). Each fix is inen.jsonand the inlinedefaultValue, so catalog and source can't drift. zh was already right in both, so it is untouched.One more instance exists outside this PR, on main and not folded in because it is neither one of the 178 added keys nor a string this PR touches:
podChat.agentRoomLiveness.timedOutreads "still in this room" in en with zh此对话中("this conversation"). Worth its own row — the zh chose a third noun, so it is a copy call, not a sweep hit.An en-render check: every added en value equals the
defaultValueit replaces byte for byte, which is what the 86 passing render tests exercise —tools.toRoom,githubAppSetupHintandnoSeatsaccepted, which is precisely the three strings that change visible English at the render gate.Verification
6c827f93(lily-shen, message 75058), verified from the source rather than from notes.localizeWindowresolves throughtime.age.hours={{n}}小时and the zh template is 「每 {{window}}可调用 {{calls}} 次」, so the budget renders 「每 1小时可调用 5 次」. Her read also closes this PR's last open copy question: the app's other zh time strings (「5分钟前」) put no space between number and unit, so main'stime.age.*family is the house style and feat(i18n): localize Connectors and Tools in zh-CN #1717's spaced form is not wanted — a call I had recorded as open rather than decided quietly. Her earlier retraction (75055) was reasoned from965b016d; at6c827f93the unit is Han, so the closed space is correct and reverting it would introduce the defect. The head has not moved since:git ls-remoteand the local worktree both read6c827f93, tree clean, so the PASS is against the branch head and not a superseded one.translationKeys.test.ts4/4;V2ConnectorsPage+V2ConnectorTools+ bothi18nsuites 86/86. The 2 warnings in the lint run below are pre-existingreact/jsx-filename-extension, not new.tools.mayUsedeleted from both catalogs, entries present →resolves every literal translation key used by a migrated componentFAILS, naming the file and both locales (en:tools.mayUseandzh-CN:tools.mayUse).eslint(local 8.57.1; notenpx eslintresolves to a global v10 that cannot read.eslintrc.jsat all) on both files: 0 errors, 2 pre-existing warnings — so ux-lead's "expect lint hits beyond the t() keys" did not materialise. Control: a literal inserted into JSX inV2ConnectorTools.tsxerrorsdisallowed literal string: <p>untranslated control probe</p> i18next/no-literal-stringat the inserted line, so the clean run is the rule running, not the rule being off.mainlocally and re-ran: 215/215 on the touched suites, 113 suites / 1009 green,tsc --noEmitexit 0.tools.dayshad the one inconsistent value, and the census that found it also undercuts the reason. ux-lead's post-gate note: the 结束时间 select offered7 天后immediately beside3天后结束。. Measured over this PR's 186 added zh keys: exactly one is spaced (tools.days), six are unspaced (the newtime.age.*family) — it was lifted verbatim from feat(i18n): localize Connectors and Tools in zh-CN #1717 instead of spelling the construction this PR ships. Fixed at3d6e1763.inviteModal.options.days77 天,podChat.catchup.hoursAgo,activity.windows.7d) against 4 unspaced — and three of those four are this PR's newtime.age.*keys, the fourth isyourTeam.card.age.days. So "no space, it matches the other time strings" was true of the sample it was checked against and false of the catalog: the new family introduced a convention the catalog mostly does not use. Lily-shen ratified the unspaced form on the partial evidence, and it is what both gates rendered, so it ships — but the respace is a catalog-wide question and is filed rather than decided here.Math.max(1, …)inlocalizeWindowpassed the entire suite while changing what a sub-minute window renders. They proposed awindowMs: 30000case; measured, 30000 is exactly0.5minutes andMath.round(0.5) === 1, so that case survives the mutation and would have been a control that cannot fail.1msis the killing case. Control, run with the tree restored in between: clamp removed →0m/0分钟,1 failed / 2 passedin that file and only there; clamp restored → 243/243 across the six touched suites. The assertions are at the unit (localizeRelativeTime.test.ts) rather than through a component, because the component fixture happens to use a one-hour window — which is exactly why the line was unguarded.null === nullcomparison would have reported all 178 as "untranslated"). Exactly 1 zh value is byte-identical to English,connectors.enableCommand=/commonly-enable {{code}}, which is a literal slash command.Not verified
defaultValues plus the passing render suites, not from a visual diff.:506-adjacent sites and the twoV2ConnectorsPageline builders are covered by the tests above as en behaviour; their zh spacing is a render-gate question, not a test question.Base is
48308534, 1 behind (#1980, tests + comments only, no catalog or component overlap);MAX_BEHINDis 40.