Skip to content

console: the standalone /forms/:name route renders select option labels untranslated — a zh-CN user on an en-US browser reads Chinese field labels over English options (FormPage normalizeOptions skips the localizer) #26452

Closed

Description

@objectstack-fleet

QA-source: #26450 · i18n.surface-matrix · n1

Found in the 18.2.0 wave-2 Tier 1 sweep (record above, which carries the full evidence chain) at framework 756220c4. Reproduced twice by the live runner and re-derived independently on a fresh boot.

What happens

On the console's standalone form route /_console/forms/OBJECT.form?recordId=ID&recordObject=OBJECT, a user who has chosen zh-CN as their language sees field labels and sections in Chinese, while every select option stays in English. The in-app overlay form (?form=ID on the object list) in the same session shows the options in Chinese. The translation bundle has the option labels.

Reproduction (stock showcase)

  1. Boot the showcase. In a browser whose locale is en-US, sign in as admin@objectos.ai through the console form.
  2. Set the user's language to zh-CN: avatar menu → Language → 中文(中国), or PATCH /api/v1/data/sys_user/USER_ID with {"locale":"zh-CN"} (answers 200).
  3. GET /api/v1/i18n/translations/zh-CN carries objects.showcase_task.fields.status.options = backlog 待规划 · todo 待办 · in_progress 进行中 · in_review 评审中 · done 已完成, and the priority options.
  4. Open /_console/forms/showcase_task.form?recordId=ANY_TASK_ID&recordObject=showcase_task and wait for the form to render.
  • Expected: status options 待规划/待办/进行中/评审中/已完成 and priority options 低/中/高/紧急, matching the zh labels on the page.
  • Actual: the labels read 标题*, 状态*, 优先级, but the selects read Backlog|To Do|In Progress|In Review|Done and Low|Medium|High|Urgent, and the comboboxes show In Review / High.
  • Control 1: /_console/apps/com.example.showcase/showcase_task?form=ID in the same session renders the options in Chinese.
  • Control 2: the same /forms URL in a zh-CN browser renders the options in Chinese. On this route, options get translated only when the server localizes the schema by Accept-Language, never from the language the user chose.

Mechanism

In ui/apps/console/src/components/FormPage.tsx, near line 848, the field mapping sets options: normalizeOptions(def.options) with no option-label translation. The neighbouring label, placeholder and helpText all go through the localizer. Per i18n.surface-matrix n1 this is a resolver FAIL: the spec declares the options group, and the bundle carries a translation for it.

Related, same route

  • The route's refusal message and its missing-recordId message are hard-coded in English.
  • Low priority: ?form=UNKNOWN_ID on the object list opens a blank edit form instead of a not-found state; the drawer handles an unknown recordId correctly with 未找到记录.

Done when

  • On every console form surface, option labels follow the user's chosen language, as field labels already do.
  • A pin covers the /forms route under a browser locale that differs from the user's language.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p1 · domain:ui · area:i18n · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T04:54Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 11 · 2026-10-27T05:26Z
    Session: session_01SMps7wq7rBTk53e8KTSEup
    Account: os-steve
    Branch: claude/issue-26452-form-options-locale
    Worktree: objectstack-issue-26452
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: ui/apps/console/src/components/InternalFormRoute.tsx and FormPage.tsx (the standalone form route's schema / option-label translation), the shared translator they should reuse if one exists, tests beside them, root .changeset/. ⛔ components form.tsx (seat 1). ⛔ locale packs under ui/packages/i18n unless the card needs a new key (then en + zh only, and say so), ui/.github/**, root ci.yml.
    Container & model: M, mode:subagent, model: default tier, effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: a zh-CN user filling a record on the standalone form route | the i18n bundle's objects.*.fields.*.options labels | every select field on /_console/forms/*
    Thread-read: none
    Premise: holds 2026-10-27T05:26Z at origin/main 00a4593ad7: the route is mounted from ui/apps/console/src/components/InternalFormRoute.tsx / FormPage.tsx; the card reproduces English option labels there while the in-app overlay form translates them.
    Serial constraints cleared: no open PR touches InternalFormRoute.tsx or FormPage.tsx.
    Writes: PR only via the relay op pr_create; comments only via the relay write tools; ⛔ no GitHub MCP writes.

    • Seat's route: find where the overlay form (?form=ID) translates option labels and make the standalone route use the same path, not a second translator. Pin red-first: zh-CN bundle with option labels, standalone route renders them translated; labels without a bundle entry keep their authored text. Fixes #26452.
  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26452,
      "status": "done",
      "branch": "claude/issue-26452-form-options-locale",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26584",
      "session": "session_01SMps7wq7rBTk53e8KTSEup",
      "premise_still_valid": true,
      "summary": "The overlay form translates nothing on the client: ObjectForm's fromObjectSchema hands field.options through as served, and the server localizes /meta/object by the request's Accept-Language, which the console's authenticated fetch (SchemaRendererContext.apiFetch) stamps with the session language. FormPage read the view, schema and record with a bare fetch, so options followed the browser's language. The internal route now sends every API request through SchemaRendererContext.apiFetch (falling back to bare fetch with no provider); the public /f/:slug route is unchanged. No second translator, no locale-pack key, InternalFormRoute.tsx untouched.",
      "tests": "Pin ui/apps/console/src/components/FormPage.optionLabelLocale-26452.test.tsx (real createAuthenticatedFetch as host apiFetch, html lang zh-CN, stub server localizing by Accept-Language). RED at 1768fd79 (pin only): 2 failed, AssertionError expected 'In Review' to contain '评审中' / expected 'Done' to contain '已完成' (run unlocked, maxWorkers=1, declared narrowing after a long lock queue). GREEN at ff64ab6a: 2 passed. At head f0294fa8 (after merging origin/main) under the lock, VERDICT command-exit 0: closure build @object-ui/console^... ok; console tests importing FormPage/InternalFormRoute: Test Files 42 passed (42), Tests 415 passed (415); pnpm --filter @object-ui/console type-check exit 0 (tsconfig includes src, so the pin is type-checked). The red-before-fix run is the ablation. Live showcase repro: NOT MEASURED, reason: showcase boot + console build + browser not cheap under the shared lock queue.",
      "gates": "Local at f0294fa8: node scripts/pm/dispatch-gates.mjs --commands derived 33; all 33 run, each exit 0 (check:query-options-erasure and check:type-check-debt hit a 400s timeout on a first pass and were re-run to exit 0; ui scripts vitest + spec check:duration-unit-keys ran under the lock: 178 files passed, 2 skipped). --ran: 33 derived, 33 run, 0 NOT-MEASURED (derived zero, all with exit codes). node scripts/check-sdui-manifest.mjs exit 0 (intact, 106 components; no registry contract change, not regenerated). CI on f0294fa8 at report time: Governed Surface Queue Guard=success, No other open PR may claim the same issue=success, all others in_progress.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls + POST issues/26584/assignees, run 40477066470); this os-dev-report comment via post-stamped (POST /repos/objectstack-ai/objectstack/issues/26452/comments). git push not counted.",
      "open_questions": [
        {
          "question": "Should the public /f/:slug route also read over the host's authenticated fetch so a signed-in visitor's chosen language reaches the server?",
          "options": [
            "A leave it on the bare fetch (this PR): anonymous visitors are unaffected; a signed-in visitor with a non-browser language still sees browser-language options there",
            "B move it onto SchemaRendererContext.apiFetch: language fixed for that case too, but the anonymous resolver and submit start receiving Bearer and X-Tenant-ID from signed-in visitors"
          ],
          "recommendation": "A, because the card's surfaces are the internal routes and B changes what the anonymous submit sees, which needs its own look"
        },
        {
          "question": "The seat's pin wording described a client bundle lookup; the overlay's path is server localization by request language, so the pin models the server and asserts the channel. Acceptable?",
          "options": [
            "A yes (this PR)",
            "B add a client translateOptions call as well"
          ],
          "recommendation": "A, because B would be the second translation path the card forbids"
        }
      ],
      "out_of_scope_findings": [
        "carrier: #26452 (card's own Related notes) · the route's English-only refusal and missing-recordId messages, and ?form=UNKNOWN_ID opening a blank edit form — noted in PR Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat 2 · 2026-10-27T10:24Z

    probe reading
    PR #26584 MERGED 2026-10-27T09:35Z, merge commit fd973515, head f0294fa8 (+238/−14, seat record on #26584)
    origin/main contains it (git merge-base --is-ancestor exit 0)
    queue branch gh-readonly-queue/main/pr-26584-*: none left (consumed)
    card closed (completed) by Fixes #26452 on #26584

    Status: the standalone form route (/forms/:name) talks to the API over the console's authenticated fetch, so option labels follow the session language like the in-app form; the public /f/:slug route is unchanged. pm:dispatched removed.

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

Metadata

Metadata

Assignees

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingdomain:uipriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions