You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Console: list-view chrome (Grid / Gallery switcher, Avg: / Sum: aggregate footer) and the form dialog's a11y fallback text are hardcoded English #4024
Migrated from objectstack-ai/objectstack#5084 under the file-at-destination ruling (objectstack-ai/objectstack#7167, maintainer 2026-08-10). Originally filed 2026-08-04T03:00:27Z. The full prior thread — including triage rulings and hold/restart conditions — remains on the source issue and MUST be read before acting on this card.
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/console 17.0.0-rc.1, browser locale zh-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, with getBoundingClientRect() used to separate visible text from screen-reader-only text:
Visible
string
where
Grid
list-view mode switcher
Gallery
list-view mode switcher
Avg: 39%
list footer aggregate, on a percent column
Sum: 119,200
list footer aggregate, on a number column
The 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)
string
where
Complete the form fields, then submit or cancel.
plugin-form modal + drawer, when the form declares no description
Close
dialog close button sr-only label
For 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:
e.description
? <Description>{e.description}</Description>
: <DescriptionclassName="sr-only">Complete the form fields, then submit or cancel.</Description>
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 TranslationData that 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:
the view-mode switcher labels,
the aggregate-footer prefixes (keyed by aggregate kind, so Avg / Sum / Min / Max / Count all land),
the form dialog's sr-only description fallback,
the dialog close button's sr-only label.
Filed from a downstream app (hotcrm-heimao), tracked there as issue #60.
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 saved
success toast after a save
Save failed
generic failure toast
Locked by environment: KEY
the SETTINGS_LOCKED refusal toast
Action failed / Action succeeded
settings action buttons
Failed to load settings
load-error card fallback
No namespace selected.
empty-route state
All settings / Back
navigation buttons
Discard / Save changes
the 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 through t('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: useSettingsLabel resolves 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 I18nProvider and assert against raw text; t() outside a provider returns the key itself, which is exactly what SettingsView.envelope.test.tsx already pins for the hub (it asserts console.settingsHub.empty appears 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.
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/Count all land, prefix from the bundle + number through the existing formatter), the form dialog's sr-only description fallback, and the dialog close button's sr-only label; (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-drift are the honesty gates.
Mutual exclusion: in-flight seats own packages/types+core type declarations (#4580), scripts/** (#4592), packages/fields/** (#4590), and packages/core/src/utils/dashboard-filters.ts + dashboard docs/examples (#4356). This card touches none of those.
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's useColumnSummary, 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/_other WITH 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.
Activity
yinlianghui commentedon Aug 13, 2026
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:Settings savedSave failedLocked by environment: KEYAction failed/Action succeededFailed to load settingsNo namespace selected.All settings/BackDiscard/Save changesN unsaved change(s)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:
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.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
yinlianghui commentedon Aug 13, 2026
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
yinlianghui commentedon Aug 13, 2026
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
fix(i18n): console chrome reaches the bundle — list switcher, aggrega…