Uh oh!
There was an error while loading. Please reload this page.
fix(i18n): one ellipsis glyph across the ten packs, es usted single-form, pt de-contraction (#3878 #3875 #3877) - #4378
Conversation
…action (#3878#3875#3877) Three locale-copy defects on one surface, all value-domain and therefore invisible to every key-shaped gate. #3878 — converge the ellipsis on U+2026 per the consistency pass registered on objectstack#6015. `en` ended 33 values with three ASCII full stops and 110 with `…`, and the nine packs had copied `en` value by value, so a user read both glyphs on one screen. 312 pack values rewritten (34 en, 278 across the nine) via the TypeScript AST over value string literals only, so keys, code and the `{{name}}`/`{{agent}}` holes were structurally out of reach. Pinned by packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts, which fails by key name on any ASCII `...` in any of the ten packs. Wider than the card asked for on purpose: `collaboration.commentPlaceholder` puts the ellipsis mid-sentence and `list.loading` had the packs wrong while `en` was already right — a trailing-only, en-only rule sees neither. The drift gate cascaded as predicted and reported 5 keys where `en` moved with nothing for the packs to follow, because on those the packs were ALREADY correct. Five ledger waivers added, each transcribing its new sentence, and WAIVER_CEILING raised 0 -> 5. Eleven inline `defaultValue` call sites and fifteen module-local no-provider fallback entries were re-synchronised with the new `en` text; both are required, not optional — check-i18n-call-site-keys.mjs enforces the first byte-for-byte and objectui#3440 enforces the collaboration map's parity. #3875 — es `preview.empty.notReadyDescription`: `Revisa` -> `Revise`. The one tú imperative in a 23:1 usted namespace, rendering underneath the usted draft bar at the same moment. `approvalsInbox` is legitimately tú and untouched. #3877 — pt `detail.concurrentUpdateDescription` left a bare `de` in front of the `{{field}}` hole, so the multi-field branch rendered `de este registro` where Portuguese must contract to `deste`. The outer sentence is rewritten so the hole follows the VERB `afeta`: swapping `de` for `em` or `a` would have re-created the same defect with a different contraction. pt only; `en` unchanged, so no drift event. The #3546 slice-7 current-truth pin is flipped in the same change, along with the slice-7 assertion that `dashboard.loading` and `common.loading` were different strings — that difference WAS this bug. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
… copy (#3878) Eight test files asserted a rendered string that objectui#3878 moved to U+2026 — either the en pack value under a provider, or the module-local no-provider fallback map, which is byte-identical to it by contract. Caught by CI shards 1-3 and by the local run; each one is the pinned copy following its source, not a weakened assertion. data-table-manual-search table.search placeholder selector serverSearch (plugin-grid) table.search placeholder selector complex-widgets (fields) table.search x2 LookupField.hydration common.select negative assertion — its own comment already said "Select…" while the assertion said ASCII RecordActivityTimeline.loading detail.loading record-chatter.loading detail.loading ConversationsSidebar console.ai.searchChats comment-thread-no-provider collaboration.replyingTo / commentPlaceholder Deliberately NOT moved: literals that are the test's or component's own hardcoded English with no pack key behind them (AuthProvider's loadingFallback prop, the plugin-designer grid mock, combobox/select/loading/command defaults), and the two docblocks that quote the retired spelling as history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…is concatenated in code, not a pack value (#3878) Correcting my own over-reach in the previous commit. The two LookupField cases in complex-widgets.test.tsx assert `getByPlaceholderText('Search...')`, and I read them as pinning `useFieldTranslation`'s `'table.search'` default, which objectui#3878 moved to U+2026. They are not: `LookupField.tsx:1085` builds the placeholder as placeholder={t('common.search') + '...'} so the ellipsis is a literal concatenated at the call site and never came from the pack at all. Moving the assertions turned both tests red — caught by the local targeted run before the CI shard reported it. Reverted to ASCII, with a comment at the assertion naming line 1085 and objectui#4375 (filed for this and the identical ListView.tsx:2829 concatenation) so the next reader does not repeat the lap. The other seven files in the previous commit are unaffected: data-table.tsx:1593 and ConversationsSidebar.tsx:251 read `t(key)` directly, so their copy really did move with the pack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 11, 2026
Verification, and one correction worth readingThe correction: |
| command | result |
|---|---|
node scripts/check-i18n-en-drift.mjs | exit 0 — 34 en changed, 269 pack values followed, 5 waived by the ledger |
node scripts/check-i18n-call-site-keys.mjs | exit 0 — 889/892 literal inline defaults match their en value (878 before) |
node scripts/check-changeset-presence.mjs | exit 0 — 1 changeset for 9 released packages |
node scripts/check-changeset-no-major.mjs | exit 0 |
node scripts/check-control-bytes.mjs | exit 0 — 4086 tracked text files scanned |
| ASCII-ellipsis census over the ten packs | 0 values remaining (312 before) |
The new pin, both directions
Written and run before the value changes, against unmodified packs: 4 of 4 tests red, naming all 312 offending values. After: packages/i18n/ plus the drift-gate suite at 43 files / 821 tests passed.
#3877's flipped assertions, both directions
The #3546 slice-7 pin was flipped to the new pt sentence. Composing the template with detail.concurrentUpdateRecordLabel exactly as ConcurrentUpdateDialog.tsx:124 does:
OLD (origin/main): …uma versão mais recente de este registro durante sua edição.
toContain('mais recente que afeta este registro') FAIL
not.toContain('de este') FAIL
template toContain('que afeta {{field}}') FAIL
NEW (this PR): …uma versão mais recente que afeta este registro. Escolha…
all three PASS
Composed across all ten packs, only pt changed shape, and the nine others remain correct as written (es de este registro, fr de cet enregistrement, de von diesem Datensatz, ru этой записи) — which is the measured basis for the single-locale divergence.
Generated by Claude Code
yinlianghui
commented
Aug 11, 2026
ACCEPT — PM 复核 (session
Flipping ready + arming auto-merge now — per the report's own risk note, this PR's ten-pack surface is worth landing early. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#3878
Fixes#3875
Fixes#3877
Three locale-copy defects on one surface (
packages/i18n/src/locales/*.ts), dispatched as one card. All three are value defects: every key name, key set and placeholder shape was already correct, which is exactly why the three script gates were green on all of them.1. One ellipsis glyph across the ten packs (#3878)
Ruling applied: converge on the typographic ellipsis
…(U+2026), per the maintainer-authorized consistency pass registered on objectstack#6015 (2026-08-09). Not a fresh call — this PR executes it.enended 33 values with three ASCII full stops and 110 with…, and the nine translation packs had copiedenvalue by value, so the split propagated rather than being a translation habit. It is same-screen visible:common.loading(Loading...) renders on the same dashboard asdashboard.loading(Loading…), andconsole.ai.askAnythingsits in the AI panel beside siblings that already used….312 pack values changed — 34 in
en, 278 across the nine. The rewrite ran through the TypeScript AST over value string literals only, so keys, code and the interpolation placeholders were structurally out of its reach;git diff --statis 312 insertions and 312 deletions, one line per value.Census corrections the card did not have
The card measured 32 en values on 2026-08-08. On
origin/maintoday it is 33 trailing, plus findings the card's trailing-only census could not see:appManagement.searchPlaceholdercollaboration.commentPlaceholderAdd a comment... (use @ to mention)), and all nine packs already wrote…collaboration.replyingTolist.loadingenwas already…, all nine packs were still ASCIIennever changed, so the drift gate had no event to fire on and the nine were free to sit thereThose two are why the pin below is wider than the card asked for.
The convention is pinned
New:
packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts. It fails, by key name, on any value in any of the ten packs holding three ASCII full stops, plus a complement assertion that the 35 converged keys still hold an ellipsis at all (so deleting the ellipsis cannot pass the ASCII scan by producing nothing), and a placeholder-integrity check.The card asked for "a trailing ASCII
...in anenvalue errors". The scan is deliberately wider on both axes because, per the table above, the narrow rule would have shipped with two holes in it. It also needs no per-key list: a new value arriving with a typewriter ellipsis fails without anyone editing the file. Same reasoning and same final shape asde-quote-pairing-3876.test.ts, whose per-key list became "no U+0022 in thedepack at all".Pre-fix RED, post-fix GREEN
The pin was written and run before the value changes, against unmodified packs:
Captured to a file, not piped through
head— the count in that message is read from the full output.The drift cascade, and five honest waivers
scripts/check-i18n-en-drift.mjsdid exactly what the card predicted — that cascade is the mechanism working. It reported 5 keys whereenmoved and a pack did not:On these five the packs were already right and
enwas the outlier — the inverse of the drift this gate hunts, and the one shape its waiver ledger exists for ("if a translation genuinely does not need to change, say so"). Five entries added toscripts/i18n-en-drift-baseline.json, each transcribing its new sentence verbatim so it expires the next time the key changes, andWAIVER_CEILINGraised0 → 5inscripts/__tests__/check-i18n-en-drift.test.ts— the ratchet that makes growing the ledger a reviewed act. Gate now exits 0 with5 finding(s) waived by the ledger. The other 269 pack/key pairs followed normally.Two duplicate-copy surfaces moved with the packs — both required, neither optional
The card warned about the #4294 "11th copy" trap in
LIST_DEFAULT_TRANSLATIONS. That specific table is clean (list.loading/list.refreshingwere already…, and it holds no copy of any of the 35 keys), but the trap exists twice elsewhere, and in both places an existing gate or contract forces the copy to follow:Eleven inline
defaultValuecall sites.scripts/check-i18n-call-site-keys.mjscarries a byte-exact rule (objectui#3810) and went red on 11 defaults across 8 keys the momentenmoved. Its own instruction is to fix it at the call site — "copy theenvalue in byte-for-byte, ellipsis and capitalisation included. Do NOT editpackages/i18n/src/locales/en.tsto match the call site". Done, in 7 files underapps/console/,packages/app-shell/andpackages/fields/. Gate back to exit 0, literal inline defaults matching 878 → 889.Fifteen module-local no-provider fallback entries, in
useCollaborationTranslation,useFieldTranslation,useDetailTranslation,ObjectGrid,KanbanImpl,data-tableandConnectionStatus. These render when noLocalizationProvideris mounted, and each map's docblock requires it to stay byte-identical to theenpack — for the collaboration map that is mechanically enforced (objectui#3440,presence-avatars-i18n.test.tsxasserts the map deep-equalsen.collaboration), and that test went red on this change, which is how the requirement was found rather than assumed. Leaving the others behind would have left the provider-less path disagreeing with the provider path on ten keys while each file's own header said it must not.useFieldTranslationwas already internally split —'lookup.loading': 'Loading…'sat two lines under'common.loading': 'Loading...'.packages/i18n/src/useSafeTranslation.tswas not touched (#3865 in flight); nor wascontent/docs/**(#3857/#3859) orpackages/data-objectstack/**(#4363).Exclusions
No
envalue was excluded — none of the 34 uses...as load-bearing syntax; all 34 are user-facing ellipsis on placeholders, loading text and progress buttons, and the{{agent}}/{{name}}holes are asserted untouched.Left alone on purpose, filed rather than smuggled in: ASCII ellipsis that is not a copy of a pack value —
ListView.tsx'st('list.search') + '...'concatenation (#4375), the metadata-admin designer's ownen/zhtable (#4377), and the hardcoded key-less component defaults (combobox,select,loading,KanbanEnhanced,LayoutRenderer), which are instances of the already-open hardcoded-English family (#4028 and siblings) and were not filed again as a twin. The new pin's header records this scope explicitly, so its green is not read as a claim about those surfaces.2. es draft-preview empty state speaks usted (#3875)
preview.empty.notReadyDescriptionsaidRevisa la conversación— the tú imperative — inside a namespace that is 23:1 usted, and it renders underneath the ustedDraftPreviewBarat the same moment rather than before or after it. One token:Revisa→Revise. The rest of the sentence is third-person statement and carries no register.Scope held to that one value, per the card's own warning:
approvalsInboxis a legitimately tú neighbourhood and was not touched, and no regex register sweep was run (the card documents five false positives that one produces).3. pt no longer contracts
deonto its own hole (#3877)Direction A, as ruled.
ConcurrentUpdateDialog.tsx:124splitsdetail.concurrentUpdateDescriptionon the literal{{field}}into a before/after pair and renders the bolded label in the gap. pt left a baredein front of that gap, so when the multi-field branch passes the record label (detail.concurrentUpdateRecordLabel=este registro) a Portuguese user read:No spelling of the leaf could fix it —
deste registrorendersde deste registro— because the preposition lives in the outer sentence.Before:
Outro usuário salvou uma versão mais recente de {{field}} durante sua edição. Escolha como resolver o conflito.After:
Outro usuário salvou, durante sua edição, uma versão mais recente que afeta {{field}}. Escolha como resolver o conflito.One deviation from the ruling's illustrative example, on purpose
The ruling offered
…do seguinte campo: {{field}}…as an example. This PR does not use it, for two measured reasons:do seguinte campo: este registroreads "of the following field: this record", and the multi-field branch is precisely the record-level summary — so that phrasing mis-describes exactly the case the bug is about.em+estecontracts toneste,a+aqueletoàquele,de+otodo. Putting the verbafetain front of the hole means no contraction rule can apply to whatever the leaf holds, now or later.The ruling's example was prefixed "e.g."; the binding constraints — hole not preceded by a bare
de,{{field}}exactly once, pt-only — are all met, and the exactly-once split contract is now asserted in the pin test.Single-locale divergence justified: the nine other packs need no rewrite because none of them contracts across this boundary (es
de este, frde cet, de dativevon diesem, ru genitiveэтой записиare all correct as written), andenis unchanged so the drift gate has no event here. pt alone diverges in sentence shape; that is cheaper than nine gratuitous rewrites.#3546 slice 7 has landed — the current-truth pin is flipped
Contrary to the triage note,
packages/i18n/src/__tests__/residue-namespaces-3546.test.tsxdoes exist on main and did pinmais recente de este registroas current truth. Flipped in this change tomais recente que afeta este registro, with an added negative assertion thatde estecannot come back, and the comment rewritten to explain the verb choice.Two more assertions in that file and in
i18n.test.tspinned the pre-ruling ASCII spellings and moved with it. One is worth naming: slice 7 asserted thatdashboard.loadingandcommon.loadingwere different strings — that difference was#3878, and it is why slice 7 could not reuse the neighbour's translations. It is now an equality, kept rather than deleted because it is the only place the convergence is visible from the slice-7 side.Verification
pnpm exec vitest run packages/i18n/ scripts/__tests__/check-i18n-en-drift.test.tspnpm exec vitest run(full repo, repo root per AGENTS.md)node scripts/check-i18n-en-drift.mjsnode scripts/check-i18n-call-site-keys.mjsnode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsnode scripts/check-control-bytes.mjsChangeset:
.changeset/i18n-copy-trio-3878-3875-3877.md, patch (never major, per the version-alignment rule) for the nine released packages the presence gate names.Out-of-scope findings filed
t(key) + '...'— the ellipsis is concatenated in code, so it stays ASCII in all ten locales and cannot be translated away #4375 —ListView.tsx:2829builds the list-search placeholder ast('list.search') + '...'. The ellipsis is concatenated in code, so it stays ASCII in all ten locales and no pack can opt out of it; sharpest inar, where a left-to-right run is appended to right-to-left text. Two fix directions given; the contract-first one adds or reuses a pack key, which is why it is not done here.list.loadingserves the raw English sentence "Loading records…" in eight of the nine packs (zh alone translated it) — plus 3 more prose values byte-identical to en #4376 —list.loadingserves the raw EnglishLoading records…in eight of nine packs (zhalone translated it), plusdesigner.undo,designer.redoandappDesigner.snakeCaseHint. Surfaced becauselist.loadingis one of the 35 keys this pass touched. Includes the full 26-key byte-identity census separating these from the ~14 legitimate identities (JSON,Webhook,name@example.com,v{{version}}, …).finding) — the metadata-admin designer's ownen/zhtable keeps ASCII...on 10 entries, so that panel now disagrees with the console chrome around it. Created by this convergence, hence recorded with it.Deliberately not filed: the hardcoded key-less component-default placeholders. Searched first — they are instances of the open hardcoded-English family (#4028 and siblings), and a fresh card would have been a twin.
Generated by Claude Code