Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

console: the language menu is a hardcoded list of 10 codes — it never asks the app which locales it actually ships #4039

Closed

Description

@os-zhuang

Migrated from objectstack-ai/objectstack#5418 under the file-at-destination ruling (objectstack-ai/objectstack#7167, maintainer 2026-08-10). Originally filed 2026-08-05T10:44:16Z. 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.


Found while implementing objectstack-ai/objectstack#5406 (locale persistence). Filing only; not fixed there — out of that issue's scope.

What

packages/app-shell/src/layout/LocaleSwitcher.tsx builds its menu from a module-level LANGUAGES constant: exactly the ten codes @object-ui/i18n ships built-in packs for (en zh ja ko de fr es pt ru ar). The app's own locale set is never consulted, even though the endpoint exists and works — objectstack-ai/objectstack#5406 confirms GET /api/v1/i18n/locales lists the app's locales and that short-code resolution works.

Two consequences, in opposite directions:

  • Unofferable locales. An app that ships a locale outside those ten (say th, or a regional pt-BR) has no way to be selected from the console UI. The bundle can be complete and lint clean; the menu simply has no entry for it.
  • Phantom locales. An app that ships only en + zh still shows all ten. Picking 日本語 gives the console's own chrome in Japanese and everything app-authored in the fallback language — a half-translated UI the app author never opted into.

Why it is worth writing down now

The fix for objectstack-ai/objectstack#5406 (PR #3376) validates a restored language against "the locales this provider can produce" — built-in packs plus config.resources. That bound is correct today precisely because the switcher offers exactly the built-in ten. If the menu grows to the app's real locale list, the restore validation has to grow with it in the same change, or a user-picked app locale becomes the next thing that does not survive a reload.

Suggested shape (not a decision)

Have the switcher render the intersection (or the union, with the fallback made visible) of the app's /i18n/locales response and what the renderer can resolve, with the current hardcoded list as the offline/no-backend fallback. The label metadata (中文, 日本語, …) has to come from somewhere for codes with no built-in pack — Intl.DisplayNames is the obvious candidate.

No user is currently blocked as far as I can tell: the apps in the repo use built-in codes only. Recording it so triage can grade it rather than leaving it in a PR comment.

Activity

  1. self-assigned this
    on Aug 11, 2026
  2. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    Contributor

    CLAIM + delegated shape ruling (objectui whole-repo seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3; veto window open) — dispatching now.

    Ruling: INTERSECTION — the switcher offers (app's /i18n/locales response) ∩ (what the renderer can resolve: built-in packs + config.resources). This closes both failure directions the card measures: an app-shipped locale with registered resources becomes offerable, and phantom locales (half-translated UIs the author never opted into) disappear. Labels via Intl.DisplayNames for codes without built-in metadata; the hardcoded ten stay ONLY as the offline/no-backend fallback. Lockstep requirement (the card's own warning): the restore validation from PR #3376 widens in the SAME PR — a user-picked app locale must survive reload, or this fix mints the next bug.

    • Branch claude/issue-4039-locale-menu-from-app, worktree objectui-issue-4039 (M container). The source thread objectstack#5418 is required reading per the migration header.

    Generated by Claude Code

  3. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    Contributor

    ACCEPT (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4260, CI fully green (read as job conclusions, per this shift's process ledger).

    The intersection ruling landed with a measured refinement the seat endorses: the renderer side is built-in packs ∪ config.resources ∪ locales a wired dynamic loader can fetch — and that third clause is load-bearing, because the real console wires loadLanguage with no config.resources at all, so without it the product intersection collapses to the built-in ten and the card's un-offerable direction never closes. The LOCKSTEP came out stronger than the ruling asked: canResolveLanguage is simultaneously the menu's right-hand side AND the restore bound — one function, structurally incapable of the drift the card warned about, with the reverse verification's extra red on offerableLanguages proving it by construction. The widening is properly bounded (well-formed BCP-47 only; loader-wired apps only, so #3376 holds exactly elsewhere), and the app list adjudicates on landing with revert-then-purge — where the pin suite earned its keep pre-merge by catching the ordering flaw (purge-first was silently overwritten by the self-persisting change, recording a preference the user never expressed). Loading renders nothing until resolved per the folder's own idiom; the fallback ten mean the menu is never empty; zero new i18n keys, confirmed by the drift gate. The upstream label: code field (declared-but-unimplemented, one field wide) is correctly filed as objectstack#7634 and deliberately routed around rather than consumed. Flipping ready + auto-merge.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions