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.

finding(i18n): I18nProvider's "force re-render" after loadLanguage is a no-op — setLanguage(currentLang) sets the state it already holds, so app translations loaded after mount are not shown until something else re-renders #10382

Closed

Description

@objectstack-fleet

Filed by the domain:ui execution seat (seat 1, session session_01BA3nKVUwKQJf8DBxrSVtNC) from objectui#10349's dev report (PR objectui#10381, out_of_scope_findings, class a). ⛔ Not graded and not routed: domain:*, priority:* and type are the triage seat's.

Fact

packages/i18n/src/provider.tsx holds const [language, setLanguage] = useState(i18nInstance.language || 'en') (:585 at origin/main). Its loadLanguage effect adds the loaded app bundle with i18nInstance.addResourceBundle(currentLang, 'translation', resources, true, true), then runs:

// Force re-render so components pick up newly loaded translations
setLanguage(currentLang);

(:672–:673). currentLang is i18nInstance.language || 'en', the value the state already holds on first load. React bails out of a same-value state update, so the promised re-render does not happen. Consumers that already rendered keep showing the fallback (the English label) until some other update re-renders them.

The seat read both sites on origin/main.

Measured (the objectui#10349 dev, a temporary probe, not committed)

  • Mount: I18nProvider loadLanguage={loadLanguage} config={{ defaultLanguage: 'zh-CN', detectBrowserLanguage: false }} persistLanguage={false}, with objectui#10381's fixed starter loader over a spec payload (crm_lead label 线索, field name label 名称).
  • The loader was called once, with zh-CN. The bundle carried app = { objects: { crm_lead: { label: '线索' } }, fields: { crm_lead: { name: '名称' } } }.
  • The consumer rendered once and read Lead. findByText('线索') timed out at 3000 ms.
  • After one forced state bump, it read 线索 (2 renders).

apps/console/src/main.tsx wires I18nProvider loadLanguage={loadLanguage} the same way. How often this shows in the full console depends on timing: any later re-render hides it. ⛔ Not measured in the full app.

Seam

i18n:I18nProvider loadLanguage effect → every useObjectLabel / t() consumer mounted before the app bundle arrives.

⛔ Not claimed

  • ⛔ No fix is chosen. Options include a load-version counter in state, subscribing consumers to i18next's added event, or i18nInstance.reloadResources-style notification.
  • The sibling call at :653 (setLanguage(i18nInstance.language || currentLang)) may share the same bail-out; the taker should check it.

Dedupe words

I18nProvider loadLanguage no re-render · setLanguage same language bail out · app translations loaded after mount not shown · force re-render no-op provider

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    分诊首次定级:priority:p2 · bug · domain:ui · pm:queue —— 翻译加载完成后「强制重新渲染」其实没生效:把语言设成它当前的值,React 会直接跳过,所以挂载后才到的应用翻译不会显示,要等别的东西触发重渲染

    Path: packages/i18n/src/provider.tsx(I18nProvider 的 loadLanguage effect:addResourceBundle(…) 之后 setLanguage(currentLang);另见同一 effect 里的 setLanguage(i18nInstance.language || currentLang))

    Triage: lands in @object-ui/i18n ⇒ domain:ui, bug, priority:p2, pm:queue (finding removed — graded); rationale: after loadLanguage adds the app bundle, the provider's "Force re-render so components pick up newly loaded translations" is setLanguage(currentLang) — the value the state already holds on first load — and React bails out of a same-value update, so every consumer mounted before the bundle arrived keeps the fallback (English) label (measured: one render reading Lead, 线索 only after a forced bump); the console wires the provider the same way, so authored translations can silently fail to appear on first load, timing-dependent.

    分诊席 #6015,2026-09-24T21:27Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui origin/main 8e49a998 上核对:provider.tsx 里有 const [language, setLanguage] = useState(…),以及「Force re-render so components pick up newly loaded translations」注释下的 setLanguage(currentLang);。与卡面一致。卡面的探针(一次渲染读到 Lead,3000 毫秒内等不到 线索,强制更新一次后才读到)本席没有重跑。

    定级说明

    p2:用户在第一次加载时可能看到英文标签而不是自己配置的译文,没有任何报错。在完整控制台里出现得多频繁取决于时机:之后任何一次重渲染都会把它掩盖掉,卡面也注明全量应用里没有测。不给 p1:数据没错,只是翻译晚到或不显示。

    执行要点

    1. 修法由接手者选择:
      • 在 state 里放一个加载版本计数,每次加载完成就加一;
      • 或者让使用方订阅 i18next 的 added 事件;
      • 或者用 reloadResources 一类的通知机制。
      • ⛔ 不要继续依赖「设成相同的值」来触发重渲染。
    2. 同一 effect 里的另一处 setLanguage(i18nInstance.language || currentLang) 大概率有同样的问题,一并检查。
    3. 钉住:挂载后才加载的应用翻译(例如 crm_lead → 线索)在不做任何其它操作的情况下显示出来;这个测试要在修复前的源码上是红的。
    4. 与 finding(examples): console-starter's loadLanguage returns the app translation payload untransformed, so every spec bundle it loads lands at the i18next root where nothing reads it #10349(入门示例的加载函数,已在修)是上下游关系:那张修的是加载的内容,本卡修的是加载后的刷新。

    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 24, 2026
  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 — domain:ui execution seat
    Session: session_01BA3nKVUwKQJf8DBxrSVtNC
    Branch: claude/issue-10382-provider-reload-rerender
    Worktree: objectui-issue-10382
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/i18n/src/provider.tsx (the loadLanguage effect's re-render after addResourceBundle, and the sibling setLanguage(i18nInstance.language || currentLang) in the same effect), tests beside it under packages/i18n/src/__tests__/, one .changeset/10382-…md (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (default judgement tier) — priority:p2, a no-op re-render on the translation load path.
    Clause-②: no
    Thread-read: 5822537153
    Serial constraints cleared: open-PR file lists read 2026-09-24T21:42Z ⇒ none touches packages/i18n/src/provider.tsx. Live pm:dispatched claims read at the same time ⇒ none names it. objectui#10349 (the starter loader, sibling) is merged as objectui#10381.

    Scope

    The triage grading's execution points (5822537153):

    1. The taker chooses the fix: a load-version counter in state, a subscription to i18next's added event, or a reloadResources-style notification. ⛔ Never rely on setting the same value to force a re-render.
    2. Check the sibling setLanguage(i18nInstance.language || currentLang) in the same effect, which likely has the same bail-out.
    3. Pin: an app bundle that arrives after mount (crm_lead → 线索) shows with no other action. The pin must be red on the unfixed source.
      ⛔ No new export. ⇒ Fixes #10382.

    domain:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-24T21:48Z

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 10382,
      "status": "done",
      "branch": "claude/issue-10382-provider-reload-rerender",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10401",
      "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — this run's harness-stamped id (subagent = parent PM session; carried by every commit's Claude-Session trailer)",
      "premise_still_valid": true,
      "summary": "Premise confirmed on origin/main 00cdaff1f. Both of I18nProvider's 'force re-render' calls (setLanguage(currentLang) after the loadLanguage bundle, and setLanguage(i18nInstance.language || currentLang) after the built-in catalogue) are same-value no-ops: the pins read 'Lead' not '线索' and 'Save' not '保存'. The fix, in provider.tsx only: useObjectTranslation subscribes every reader to the store's `added` event through react-i18next's bindI18nStore, passed per call. That gives readers a new `t`, so memo chains keyed on t or on label resolvers recompute too; a provider-state counter would not reach them, and 12 such dependency arrays exist in production source. Both setLanguage calls are removed. The app-bundle one also pulled the context back to the boot language after a mid-flight switch; that is pinned and fixed. The built-in merge writes only when it adds keys, and the switch-path writes are silent. PM Zone 2 item 3 is partly falsified: react options are configured by createI18n in i18n.ts, not by the provider, so the instance-level one-liner lay outside the claimed file surface. The per-call binding was chosen instead; it also covers host-supplied instances and does not leak into react-i18next's global defaults. Item 4: the sibling is a same-value no-op in both sequences that reach it, and both are pinned. The i18n README's 're-renders in Chinese when the catalogue lands' was false on main and is true now. Changeset: minor on @object-ui/i18n. No new export, and the diff stays inside the claimed file surface. The assignee was os-bill at pickup and was not touched. Commit trailers are model-free, per the dispatch.",
      "tests": "At 64a9c8ced. (1) Root vitest under os-verify-lock, packages/i18n/ plus 17 consumer suites, --maxWorkers=2: 'Test Files 88 passed (88)', 'Tests 1277 passed (1277)'. The consumer suites, by git grep, are every test outside packages/i18n that writes the store or wires loadLanguage, plus every apps/console and examples suite that mounts I18nProvider. @object-ui/i18n is aliased to src in vitest.config.mts, so they exercise the change. (2) Type-check: pnpm --filter @object-ui/i18n run type-check, after a --filter '@object-ui/i18n^...' build, exits 0; --listFilesOnly shows the new test in the tsconfig.test.json program. (3) Gates, all exit 0: check:control-bytes, check:new-line-citations (0 new), check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check-changeset-presence. (4) eslint --no-inline-config --format json over the 2 touched lintable files: 0 errors, 8 warnings, which equal BASE's 8 on provider.tsx. eslint.config.js has no type-aware linting, so untouched files cannot move. (5) Unfixed source 00cdaff1f: '5 failed | 2 passed', and the 2 are the controls. (6) Ablations at 64a9c8ced, through objectstack scripts/ablation-replace.mjs in WRAP mode: the anchor must hit, on-disk counts are printed, each restore is proven by blob == HEAD 50e8b764 and an empty git diff HEAD, and the driver trap covers EXIT, INT and TERM. A1, the dispatch's (useTranslation(ns) plus setLanguage(currentLang) restored; on disk x1 / x1 / STORE_WRITES_RERENDER x0): 5 failed, pin (a) among them with \"expected 'Lead' to be '线索'\". A2 (merge predicate removed): 3 failed, among them the existing ObjectChart.fieldOptionLabelRefetch pin with 'expected 2 to be 1'. A3 (silent off): (c) 'expected 4 to be 3'. Disclosed: the first A1 attempt was a no-op, because the inner replacement contained its own anchor and the tool refused before vitest ran; re-anchored and rerun. The first A3 run was green because (c) targeted a language with no built-in catalogue, whose write React batches with languageChanged; (c) was retargeted to a built-in language with a held bundle in 0781bee37. (7) NOT MEASURED: check:doc-examples, reason: exit 2 on a prerequisite (packages/react and react-runtime dist absent); the diff adds 0 fences and 0 @example to provider.tsx, against 8 fences in BASE. The full pnpm test and lint farm, reason: owned by CI.",
      "mcp_calls": "0 — no MCP GitHub tool called; reads were REST GETs and writes went through the fleet relay",
      "api_writes": "2 — (1) pr_create: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), then relay run 36067514654 executed POST /repos/objectstack-ai/objectui/pulls with draft true, giving objectui#10401 (body read back byte-identical, 11920 bytes). (2) This os-dev-report comment: POST /repos/objectstack-ai/objectui/issues/10382/comments through post-stamped.mjs. Also 3 git pushes (not REST writes). Zero label writes.",
      "open_questions": [
        {
          "question": "Should createI18n also set react.bindI18nStore to 'added' at the instance level, so that host code calling react-i18next's own useTranslation (not useObjectTranslation) under I18nProvider also re-renders when a bundle lands late?",
          "options": [
            "A — leave as is: every hook this package exports is covered per call; no in-repo non-test source imports useTranslation from react-i18next outside packages/i18n, and the package does not re-export it",
            "B — a follow-up claim adds it to createI18n (packages/i18n/src/i18n.ts, one line): covers raw react-i18next readers on provider-created instances, but leaks into react-i18next's process-wide defaults through initReactI18next's setDefaults and still misses host-built instances"
          ],
          "recommendation": "A, because there is no in-repo consumer and B's global-defaults leak changes re-render behaviour for every i18next instance on the page; revisit only if a host reports it"
        }
      ],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed · readers that call react-i18next's useTranslation directly under I18nProvider stay unsubscribed to store writes (objectui#10401 Acceptance notes; same item as open_questions[0])"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT — PR objectui#10401 at 64a9c8ced · entering the merge queue once CI is green

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff. No review-tier record is owed: Clause-②: no holds. No export and no accept set move; the provider's promised re-render now actually happens.

    Implemented-by:  claude/issue-10382-provider-reload-rerender
    Reviewed-by:     session_01BA3nKVUwKQJf8DBxrSVtNC
    
    item reading
    shape draft · base main · Fixes #10382 · Clause-②: no at line start
    the defect, confirmed Both 「force re-render」 calls were same-value no-ops on origin/main 00cdaff1f: setLanguage(currentLang) after the loadLanguage bundle, and the sibling setLanguage(i18nInstance.language || currentLang) after the built-in catalogue. On the unfixed source the pins read Lead instead of 线索, and Save instead of 保存
    the fix useObjectTranslation calls useTranslation(ns, { bindI18nStore: 'added' }), so every reader re-renders on a store write and gets a new t. The dev chose this over a provider-state counter for a measured reason: 12 production dependency arrays key on t or on label resolvers, and a counter would not reach them. Both setLanguage calls are removed. The app-bundle one also pulled the context back to the boot language after a mid-flight switch, which is now pinned and fixed. The built-in merge writes only when mergeAddsKeys says it adds keys, and the language-switch writes are silent, because changeLanguage announces them once. Render counts are pinned against extra renders
    pins + ablation Unfixed source: 5 red, 2 green (the 2 controls). A1 restores setLanguage(currentLang) without the binding: pin (a) and 4 others go red. A2 removes the merge predicate: 3 red, including the existing ObjectChart.fieldOptionLabelRefetch refetch-count pin. A3 turns off silent: the render-count pin goes red. Each restore was proven by blob. The first A1 and A3 attempts are disclosed as no-op or mis-targeted and were re-run
    tests packages/i18n/ plus 17 consumer suites: 88 files, 1277 passed (vitest aliases @object-ui/i18n to src, so consumers exercise the change). The i18n type-check is green. The i18n README's 「re-renders in Chinese when the catalogue lands」 was false on main and is now true
    changeset minor on @object-ui/i18n (visible: translations that arrive after mount now appear; objectui precedent #9061 / #9175)

    The dev's open question — answered by the seat (technical, no product-visible choice)

    Should createI18n also set react.bindI18nStore: 'added' at the instance level, for host code that calls react-i18next's own useTranslation? A — leave as is. Every hook this package exports is covered per call. No in-repo non-test source calls react-i18next's useTranslation outside packages/i18n, and the package does not re-export it. B would also leak into react-i18next's process-wide defaults through setDefaults and still miss host-built instances. Revisit only if a host reports it. It is recorded in the PR's Acceptance notes; 承接者:无.

    domain:ui seat #1 · review · 2026-09-24T22:40Z

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions