fix(i18n): console chrome reaches the bundle — list switcher, aggregate prefixes, dialog a11y fallbacks, SettingsView (#4024) - #4602
Merged
Conversation
…te prefixes, dialog a11y fallbacks, SettingsView (#4024) Six strings on the two screens a user looks at most were hardcoded English literals rather than bundle lookups. None corresponds to authorable metadata, so no app could work around them. Two needed no new keys at all. The list-view mode switcher named its nine visualizations from a private VIEW_LABELS table while console.objectView. viewType* — the same nine words — had been resolved through the bundle by the create-view picker for months. And the create/edit dialog's close button is the remainder of objectstack#5505: that fix reached the two Shadcn-synced primitives via the declared patch, but MobileDialogContent is a hand-written custom/ wrapper with its own close button, and it is exactly what ModalForm renders. The aggregate footer had a locale-formatted number behind a hardcoded prefix. All eleven aggregation kinds now read grid.summary.*, and the label/value join is its own key so a pack owns its separator. Numbers untouched (#4589's surface). The form dialog's sr-only description fallback joins the packs too. SettingsView converts as one unit: its sibling SettingsHub already resolved everything through t('console.settingsHub.*') while the view routed zero framing copy through i18n. All of it now resolves through console.settingsView.*, including the crypto-refusal strings #4579 deliberately deferred here. The save-bar counter was an English plural rule executing in every locale. It is now a real i18next plural family — base + _one + _other in all ten packs — with the base key carrying the CLDR categories ru and ar do not enumerate. 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. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
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.
Fixes #4024
Six strings on the two screens a user looks at most were hardcoded English literals rather than bundle lookups, so they stayed English on every non-English console with nothing an app could author to change them. This converts all of them, plus the SettingsView unit recorded in the card's comment, through the existing console bundle — adding keys to all ten packs.
Precondition check — objectstack#5084
The card was migrated from objectstack#5084 under the file-at-destination ruling, and its body requires that thread's triage rulings and hold/restart conditions be read before acting. Read in full: no hold or restart condition is in force. The thread has exactly two comments — a triage comment (
分诊:入队 pm:queue) and a migration comment closing it asnot plannedwith the words "moved, not rejected". The triage comment carries three leads, all checked againstorigin/main, all three load-bearing:ViewSwitcher.tsxGrid/Gallery hardcoded, with the bundle already carryingviewTypeGrid/viewTypeGallery— "疑似未接线". Confirmed exactly.plugin-form's a11y fallback. Confirmed, atModalForm.tsx:842/DrawerForm.tsx:642(line numbers had moved).Avg:/Sum:前缀在 origin/main 上未直接 grep 命中,可能已修或以模板串形式存在 —— 开发时请先与已落地的 i18n 修复对账,只补余量." The most valuable lead. The prefixes are alive, inplugin-grid, notplugin-list— and reconciling with landed fixes found a fourth remainder the card could not have known about; see the close button below.String census (file:line, on origin/main)
Grid/Gallery(+7 more)packages/plugin-list/src/ViewSwitcher.tsx:57-67(VIEW_LABELS, read at :122/:123/:134, :148/:156, :173, :216/:217/:229)SumAvgMinMaxCountEmptyFilledUniquepackages/plugin-grid/src/useColumnSummary.ts:70-83(TYPE_LABELS), joined at :231/:234/:269grid.summary.*Complete the form fields, then submit or cancel.packages/plugin-form/src/ModalForm.tsx:842,DrawerForm.tsx:642form.dialogDescriptionFallbackClose(sr-only)packages/components/src/custom/mobile-dialog-content.tsx:147common.closeapps/console/src/pages/settings/SettingsView.tsx:143, :167, :185, :212, :218, :234, :250, :261, :262, :264, :273, :284, :303, :309, :312, :362, :377, :381console.settingsView.*Row 5 is wider than the nine strings the comment listed: converting the screen turned up the bare
Locked by environment(no-key variant), bothCannot encrypt secretstoasts, and both halves of the crypto panel's body sentence. Those are the same refusal unit as the heading the comment names, so they convert with it rather than leaving a file split-conventioned in a new place.Two of the six needed no new keys — the more interesting half
The switcher was a wiring gap, not a missing capability.
console.objectView.viewType{Grid,Kanban,Gallery,…}already exists in all ten packs and already carries exactly these words: the create-view picker (packages/app-shell/src/views/CreateViewDialog.tsx:88-96) has resolved them through the bundle for months. Minting a parallellist.viewType.*would have duplicated nine already-translated words into ten packs and left two tables free to drift, so the picker's 「画廊」 and the switcher's 「画廊」 are now the same key. A plugin package reading aconsole.*key has in-repo precedent in this very namespace:packages/plugin-view/src/ObjectView.tsx:83resolvesconsole.objectView.new.The close button is a remainder of a fix that already landed, and only the triage comment's "只补余量" instruction surfaced it. objectstack#5505 routed the
sr-onlyclose label throughCloseSrLabel→common.close, andsheet-dialog-close-i18n.test.tsxpins it — but it patched the two Shadcn-synced primitives undersrc/ui/**through the declared patch inscripts/shadcn-local-patches.mjs.custom/mobile-dialog-content.tsxis a hand-written wrapper outside that regeneration zone, with its own close button, and it is whatplugin-form'sModalFormrenders — so the create/edit dialog the card measured was precisely where an English "Close" survived. One-line fix, no new key.Key taxonomy and the convention matched
console.settingsView.*, placed immediately afterconsole.settingsHubin every pack. The convention matched is the sibling's, exactly:SettingsHub.tsxusesuseObjectTranslationfrom@object-ui/i18n+t('console.settingsHub.*'), andSettingsView.tsxnow does the same. The comment's own framing is the rationale — the screen converts as one unit or the file stays split-conventioned.grid.summary.*, a nested block inside the existinggridnamespace, matching thegrid.toolbar/grid.importsub-namespace shape already there.form.dialogDescriptionFallback, beside the existingform.discardTitle/form.discardMessagethis file's safe-translation table already owns.console.objectView.viewType*(9),common.close(1), andconsole.settingsHub.beta— the Beta badge is the same release-stage badge on the same feature, and zh deliberately keeps it Latin under an existing entry inuntranslated-identity-4376. AsettingsView.betatwin would have needed a second allowlist entry saying the same thing.Net: 32 new keys across ten packs, 10 reused.
The label/value join is a key, not a
': 'The aggregate footer built
`${label}: ${formatted}`, which hands the locale the word and keeps the punctuation in English.grid.summary.pattern({{label}}: {{value}}) owns the whole shape instead — zh sets a fullwidth colon, fr the French space-before-colon. Same reasoning the packs already record forcollaboration.resolvedSuffix: "separator included, so a translator owns the whole phrase rather than inheriting an English-shaped glue."Locked by environment: {{key}}and the crypto refusals are parameterized the same way, never concatenated.Plural mechanism
{dirtyKeys.length} unsaved change{dirtyKeys.length > 1 ? 's' : ''}is an English plural rule executing in every locale. It is now a real i18next family — base key +_one+_otherin all ten packs.This repo has two count conventions and the choice between them is load-bearing. The
xxxCount/xxxCountOnetwo-sibling-key shape (common.itemCount,collaboration.commentCount) was not used: its stated rationale is a parity fear that objectui#3863's base-key fix has since answered, and it hard-codes a two-form English-shaped split that ru and ar do not have. The mechanism used instead is the oneperm.facetuses andall-locales-key-parity.test.tsowns.The base key is the whole point. i18next asks
Intl.PluralRulesfor the one suffix a language needs and, finding no such slot, walksfallbackLngtoen. ru has four categories and ar six; no pack here enumerates_few/_many/_two/_zero, so without a base key ru renders English at counts 2-20 — which is objectui#3863 measured ondetail.showEmptyRelated. ru and ar take the "noun: {count}" base form their own packs already document for exactly this reason (ru'sperm.facetnote: «этот приём в паке уже применён 25 раз … он верен при ЛЮБОМ числе»). Verified rendering in-language at counts 1, 2 and 5.The #4514 provider-less trap — decided per test, and per package
t()outside a provider returns the key, so this had to be decided rather than assumed. The split is by who consumes the code, not by preference:createSafeTranslation, tests unchanged.ViewSwitcher,useColumnSummary,ModalForm/DrawerFormandCloseSrLabelall resolve through the safe factory, so a provider-less host renders English rather than a raw key. This is not optional:useColumnSummaryis a public export whose own sibling suite asserts/Sum: /with no i18n provider, anddiscardGuard.test.tsxdrivesModalFormand clicks its close control by English name. Four new no-provider suites pin this explicitly rather than leaving it assumed.SettingsView.crypto-unavailable.test.tsx→ gained anenI18nProvider. Its two most valuable assertions are about interpolation (Cannot encrypt secrets: ai.api_key,Locked by environment: api_key). With no provider the key comes back bare and the{{subject}}/{{key}}holes are never filled — so re-pointing those assertions at key literals would have kept the file green while deleting the thing it tests.SettingsView.envelope.test.tsx→ deliberately still provider-less, and a note now records why. Everything it asserts is either manifest-authored content (Timezone,Branding— off the payload, not the pack) or the hub's key literalconsole.settingsHub.empty, which it pins becauset()with no provider returns the key. Adding a provider would delete the one i18n assertion in the file.Each new locale-mounted suite has its no-provider case in a separate file, following the warning
sheet-dialog-close-i18n.test.tsxrecords:createI18nregisters a react-i18next module-global default that survivescleanup(), so a no-provider render sharing a file with locale-mounted ones resolves against whichever locale ran last (it failed there naming a button "Cerrar").Red-first — verbatim
Predictions were written into every test file header before the first run.
Run 1 —
Test Files 3 failed | 3 passed (6),Tests 12 failed | 8 passed (20):Run 2 — plugin-form,
Tests 2 failed | 4 passed (6):Run 3 — SettingsView,
Tests 7 failed | 2 passed (9): all zh/ru cases red.One prediction was wrong, and it is recorded in the test header rather than quietly corrected. The SettingsView header first claimed the counter cases would go red in both languages including
en, reasoning that the bundle had nounsavedCountkey yet. Theencounter case passed pre-fix — because the concatenation it replaces is the English plural rule, so English is precisely the one language whose output cannot change. That is the defect stated exactly: the screen was correct in English and could not be correct anywhere else. Theencounter case is therefore a must-not-change pin; ru and zh carry the evidence.Reverse verification
git diff > fix.patch+git checkout origin/main -- <19 files>+git apply+sha256sum -c(all 19 OK). Nogit stashat any point.With the fix removed:
Test Files 5 failed | 4 passed (9),Tests 21 failed | 14 passed (35). The 21 red are every translated assertion; the 14 that stay green are exactly the English and no-provider must-not-change pins — the correct direction here, since an English-only assertion cannot distinguish a literal from a bundle lookup that resolves to the same word.Must-not-change
check:i18n-en-driftreports 0 en values changed, 32 added, 0 removed, and all ten packs' source diffs are purely additive (0 lines removed in every locale file).toLocaleString/Intl.NumberFormatcall inuseColumnSummaryis byte-identical;packages/core/src/utils/**untouched. Pinned positively by a case asserting the zh prefix andCN¥and1,234in one label.detail.highlightFields,detail.sortBy, allconsole.settingsHub.*keys unmoved.SUPPORTED_SUMMARY_TYPESstill derives from the total record, so a spec-added member is still a compile error.Verification
flock.pnpm exec vitest run --maxWorkers=2over the full consumption radius:Test Files 271 passed (271),Tests 3016 passed (3016)— i18n, apps/console settings, plugin-form, plugin-list, plugin-grid, components, plusCreateViewDialog.ariaRequired.test.tsx(the other consumer of the reused viewType keys).type-checkgreen on all six packages.check:i18n-call-site-keysPASS (2928 en keys, every call-site key resolves, every inline default matches, every interpolation argument matches its holes),check:i18n-en-driftPASS, plus control-bytes, phantom-dependencies, changeset-presence, changeset-no-major, changeset-fixed, type-check-coverage, lint-coverage, doc-links — all PASS.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'over all 29 touched files including untracked: clean.origin/maincompare worktree: 0 errors both sides. +10 warnings, all@typescript-eslint/no-explicit-anyon the four new test fixtures (const cols: any[],const ds: any), matching the sibling suites' existing convention verbatim.One gate caught a real mistake
A run filtered on
packages/app-shell/src/views/CreateViewDialog.test.tsx— a file that does not exist — was rejected by this repo's vitest path guard (objectui#3288) instead of exiting green withpassWithNoTests. Re-run against the realCreateViewDialog.ariaRequired.test.tsx. Recorded because the guard prevented a false green in this very PR..d.ts measured both ways (dist + tsbuildinfo cleared)
289
.d.tsfiles hashed before and after a clean rebuild. Exactly 12 moved:@object-ui/i18n.d.tsgain new readonly literal membersTranslationKeyswidens. Purely additive.@object-ui/console@object-ui/plugin-griduseColumnSummary.d.tsTYPE_LABELS→TYPE_LABEL_KEYS. No exported declaration moved.@object-ui/plugin-list.d.tsbyte-identical.@object-ui/plugin-form@object-ui/componentsNo major anywhere;
check:changeset-no-majorandcheck:changeset-fixedboth pass.Translation spot-check flags for a human reviewer
Every pack's existing register was censused first and neighbouring words reused verbatim rather than re-invented —
common.back,common.close,form.discard,detail.saveChanges,form.saveError,settingsHub.loadErrorand the existingactionFailedare lifted unchanged per pack. The genuinely new prose, worth a native-speaker eye:Несохранённых изменений: {{count}}and{{count}} تغيير(تغييرات) غير محفوظfollow each pack's own documented idiom for this exact problem, but they are the strings that must read naturally at 2, 5 and 11.cryptoRefusalTitle,cryptoRefusalSubjectSuffix,cryptoRefusalNoSubject) in all nine — new prose with no neighbour to copy, and the suffix form must grammatically follow acodeelement in each language.cryptoRefusalSubjectSuffixuses은(는), the written-form particle hedge. Correct but formal; a native speaker may prefer a rephrasing that avoids the particle entirely.grid.summary.countUnique— zh 去重 / ja 一意 / ko 고유 are terse column-footer forms; confirm they read as footer labels rather than verbs.patternis{{label}} : {{value}}(space before colon, French typography) while the other Latin packs keep:. Deliberate.grid.summary.patternis identical toenin ja/ko/ru/ar and is allowlisted inuntranslated-identity-4376as a pure format string — two holes and a separator, no prose. The packs that needed a different separator have one (zh, fr), so the identity is a translation decision rather than an omission.Surface respected
Touched none of the in-flight seats' files: no
packages/typesorpackages/coretype declarations (#4580), noscripts/**(#4592, gates run only), nopackages/fields/**(#4590), nodashboard-filters.tsor dashboard docs/examples (#4356), nocontent/docs/releases/**.packages/core/src/utils/**untouched — #4589's number-display surface is consumed, never edited.Generated by Claude Code