Repository navigation
Console: list-view chrome (Grid / Gallery switcher, Avg: / Sum: aggregate footer) and the form dialog's a11y fallback text are hardcoded English #4024
Description
Activity
Same family, one more screen — measured while implementing #4570 (PR #4579). Commenting here rather than opening a twin, since the fix shape you describe ("move through the existing console bundle") is identical.
The Settings namespace page (
apps/console/src/pages/settings/SettingsView.tsx) routes zero framing copy through i18n. Every user-visible string on it is a hardcoded English literal:string where Settings savedsuccess toast after a save Save failedgeneric failure toast Locked by environment: KEYthe SETTINGS_LOCKED refusal toast Action failed/Action succeededsettings action buttons Failed to load settingsload-error card fallback No namespace selected.empty-route state All settings/Backnavigation buttons Discard/Save changesthe unsaved-changes save bar N unsaved change(s)save-bar counter (also has an English-only plural rule) What makes it worth recording as one item rather than nine: its immediate sibling already does the right thing.
SettingsHub.tsx, in the same directory, resolves throught('console.settingsHub.*')against the same bundle that ships all ten packs. So this is not a missing capability or an unwired screen — it is one file that was written against a different convention than the file next to it, which matches your framing of these as "omissions from an otherwise complete bundle".Same "an app cannot work around it" property you note:
useSettingsLabelresolves manifest-authored labels (title, description, field labels, option labels), so a plugin author can translate the content of a settings namespace but has no key that reaches the chrome around it. A zh-CN admin sees translated field labels sitting inside an English save bar.Two notes for whoever picks this up:
- The provider-less trap (Mounting an I18nProvider in a test file silently changes what every LATER provider-less render in that file resolves #4514) is live on this screen. The existing settings tests mount no
I18nProviderand assert against raw text;t()outside a provider returns the key itself, which is exactly whatSettingsView.envelope.test.tsxalready pins for the hub (it assertsconsole.settingsHub.emptyappears as literal text). Converting SettingsView's copy will need those tests updated in the same change, or they go red for the right reason in a confusing way. - feat(console): the settings save renders the crypto-unavailable refusal as a first-class state (#4570) #4579 deliberately did not fix this, and added one more hardcoded string on purpose (
This deployment cannot encrypt secrets, the new crypto-unavailable refusal). Routing one string through i18n would have left a single translated string among a dozen hardcoded ones in the same file and dragged ten locale packs into a card scoped to one view. It mirrors the surrounding convention instead, so this screen stays internally consistent and converts as one unit whenever this issue is picked up.
Generated by Claude Code
- The provider-less trap (Mounting an I18nProvider in a test file silently changes what every LATER provider-less render in that file resolves #4514) is live on this screen. The existing settings tests mount no
CLAIM — session
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4024-console-chrome-i18n.PM ruling (delegated, veto window open): one seat converts all of it as one unit — (a) the card's five originals: view-mode switcher labels, aggregate-footer prefixes keyed by aggregate KIND (
Avg/Sum/Min/Max/Countall land, prefix from the bundle + number through the existing formatter), the form dialog'ssr-onlydescription fallback, and the dialog close button'ssr-onlylabel; (b) the SettingsView screen recorded in the comment below, including the save-bar counter through the i18n library's plural mechanism (never an English-only(s)rule) and #4579's deliberately-deferred crypto-refusal string. Rationale for absorbing (b): the comment's own analysis — the screen converts as one unit or the file stays internally split-conventioned.Binding preconditions: the source thread objectstack#5084 MUST be read first — if any hold/restart condition there is still in force, STOP and report before editing. The #4514 provider-less trap is live on the settings tests — they update in the same change. All ten locale packs gain the new keys;
check:i18n-call-site-keys+check:i18n-en-driftare the honesty gates.Mutual exclusion: in-flight seats own
packages/types+coretype declarations (#4580),scripts/**(#4592),packages/fields/**(#4590), andpackages/core/src/utils/dashboard-filters.ts+ dashboard docs/examples (#4356). This card touches none of those.
Generated by Claude Code
Generated by Claude Code
PM step-7 复核: ACCEPT. PR #4602 converts the whole unit exactly as ruled, and the census discipline paid for itself three times: (1) the source-thread precondition was honored and its '只补余量' instruction is what surfaced that the switcher was a pure WIRING gap (the bundle already carried all nine words — reused, so the picker and switcher cannot drift) and that the sr-only Close was objectstack#5505's remainder in the hand-written
custom/wrapper its own fix couldn't reach; (2) the aggregate prefixes were found in plugin-grid'suseColumnSummary, not plugin-list — why the original grep missed them; (3) SettingsView's true census was 18 literals, not 11. The plural work is the standard this repo wants: i18next native_one/_otherWITH the base key (the #3863 ru fallback trap named and verified at counts 1/2/5), the two-sibling-key convention explicitly rejected with reasons, and the one wrong red-first prediction (en counter green pre-fix — the concatenation IS the English rule) recorded in the test header as the defect statement rather than quietly corrected. #4514 dispositions decided per-test by consumer (createSafeTranslation for published components, provider gained where interpolation is the assertion, envelope test deliberately provider-less with the why recorded). en-drift 0, ten packs purely additive, plugin-grid's .d.ts delta proven comment-only by strip-diff, changesets graded per package by position analysis. CI 20/20 zero failures.Translation flags noted for a future native-speaker pass (ru/ar counter idioms, ko particle hedge, the deliberate fr space-colon) — none blocking.
Flipping ready and arming auto-merge now.
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Summary
A handful of console chrome strings are hardcoded English literals rather than bundle lookups, so they stay English on every non-English console. They are small individually, but they sit on the two screens a user looks at most — the list view and the create/edit dialog — and there is nothing an app can author to change them.
Evidence
Fresh database,
@objectstack/console17.0.0-rc.1, browser localezh-CN, admin user, every app-owned label correctly Chinese. Harvested by walking the DOM and keeping text nodes that contain Latin letters and no CJK, withgetBoundingClientRect()used to separate visible text from screen-reader-only text:Visible
GridGalleryAvg: 39%percentcolumnSum: 119,200numbercolumnThe aggregate footer is the interesting one: the number is formatted, but the prefix is a hardcoded
Avg:/Sum:. The aggregate kind is already known to the renderer, so this is a bundle lookup away.Screen-reader-only (still worth fixing — it is what assistive tech announces)
Complete the form fields, then submit or cancel.plugin-formmodal + drawer, when the form declares nodescriptionClosesr-onlylabelFor the record, the form-dialog string is not visible — we verified it is clipped (
sr-only), correcting an earlier assumption on our side that it was a visible subtitle. It is emitted as:so an app can only displace it by authoring a
description, which makes a visible subtitle appear on every dialog. Adding visible UI to every form in order to remove an invisible untranslated string is not a trade an app should have to make — the fallback should come from the bundle.Why an app cannot work around any of these
None of them corresponds to authorable metadata. They are not object labels, field labels, view labels or action labels — there is no key in
TranslationDatathat reaches them. The console's own bundle already ships zh-CN, ja-JP, es-ES, de, fr, pt, ru, ko and ar and translates hundreds of neighbouring strings (detail.highlightFields→ 「关键字段」,detail.sortBy→ 「排序」, and so on), so these are omissions from an otherwise complete bundle rather than a missing capability.Suggested fix
Move all five through the existing console bundle:
Avg/Sum/Min/Max/Countall land),sr-onlydescription fallback,sr-onlylabel.Filed from a downstream app (
hotcrm-heimao), tracked there as issue #60.