Skip to content

Console: list-view chrome (Grid / Gallery switcher, Avg: / Sum: aggregate footer) and the form dialog's a11y fallback text are hardcoded English #4024

Description

@os-zhuang

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>
  : <Description className="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.

Activity

  1. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    Collaborator

    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.

    Generated by Claude Code

  2. self-assigned this
    on Aug 13, 2026
  3. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    Collaborator

    CLAIM — session session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/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/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.


    Generated by Claude Code


    Generated by Claude Code

  4. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    Collaborator

    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.

    Flipping ready and arming auto-merge now.


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions