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 }}
Repository navigation
finding(dashboard): ObjectPivotTable carries the same i18n-resolver ref workaround #5587 is removing from ObjectChart #5625
Observation-class finding, filed unassigned while implementing #5587. Found by the over-delete sweep for that card, not by looking for it.
What is there
#5587 removes a ref wrapped around an i18n resolver in packages/plugin-charts/src/ObjectChart.tsx. Sweeping the repo for other readers of the symbol being deleted turned up a second, independent instance of the same workaround in a different package:
// i18n: translate field display labels and select option labels via the
// standard object/field translation conventions. Held behind refs so the
// metadata-derivation effect doesn't need them in its dep array (the i18n
// hook returns fresh function identities each render).
const { fieldLabel, fieldOptionLabel } = useSafeFieldLabel();
const fieldLabelRef = useRef(fieldLabel);
const fieldOptionLabelRef = useRef(fieldOptionLabel);
useEffect(() => {
fieldLabelRef.current = fieldLabel;
fieldOptionLabelRef.current = fieldOptionLabel;
}, [fieldLabel, fieldOptionLabel]);
Read at lines 108 and 116, inside the metadata-derivation effect whose dep array is [dataSource, schema.objectName] (line 127).
The stated reason — "the i18n hook returns fresh function identities each render" — is the same condition #5564 measured and PR #5585 fixed, so the same precondition is gone here too.
Deliberately not included in #5587's PR. It is a different package (plugin-dashboard, which declares @object-ui/i18n at package.json:30), a different test suite, and — this is the part that makes it a separate review rather than a mechanical repeat — the ref hides the resolvers from a metadata-derivation effect rather than from a fetch callback. Removing it changes when that effect re-runs and what it re-derives, which is not the same behaviour question #5587 answers, and it deserves its own before/after test rather than riding along.
Suggested shape, if queued
Mirror #5587: drop both refs, depend on fieldLabel / fieldOptionLabel directly in the effect's dep array, and pin it with a test that counts effect runs (or getObjectSchema calls) across re-renders both inside and outside a provider. #5587's ObjectChart.fieldOptionLabelRefetch.test.tsx is a usable template, including the ordering note about createI18n registering a process-global instance.
Worth checking as part of that work whether the two derived state maps (setFieldLabelMaps / setFieldNameLabels) can settle, since a direct dependency puts them in a loop's path the same way fetchData was in #5587's.
Concentration-round first-touch (triage seat): promoted finding → pm:queue, Task S. The precondition for the ref workaround is measured gone (#5564 / PR #5585), and the removal shape mirrors #5587 with the template test it names. The card's own two cautions are the acceptance bar: an effect-run-counting pin across re-renders (in and out of a provider), and the settle check on the two derived state maps — a loop here is a stop-and-report, not a force-through. Sequencing: after #5587 merges (its template test is the reference); not hard-blocked.
Triage graded this Task S with the precondition already measured discharged (#5564 / PR #5585) and the removal shape mirroring #5587 plus the template test it names — read that landed diff rather than inferring the shape from this card.
⚠️ The card's own two cautions are the acceptance bar, not commentary: an effect-run-counting pin across re-renders (both inside and outside a provider), and the settle check on the two derived state maps. A removal that merely compiles and renders has not met them — a loop that re-settles forever is exactly what the ref workaround was papering over.
{
"issue": 5625,
"status": "done",
"branch": "claude/issue-5625-pivottable-i18n-ref-workaround",
"pr": "https://github.com/objectstack-ai/objectui/pull/5851",
"premise_still_valid": true,
"summary": "Removed both i18n-resolver refs (fieldLabelRef / fieldOptionLabelRef and the useEffect keeping them current) from packages/plugin-dashboard/src/ObjectPivotTable.tsx, reading fieldLabel / fieldOptionLabel directly in the metadata-derivation effect and declaring them in its dep array — the shape #5587 landed in ObjectChart (PR #5628), which I read rather than inferring from the card. Precondition re-verified on current main before removing anything: useObjectLabel pins both memo deps to module constants (NO_INSTANCE_T / NO_INSTANCE_I18N) while no i18next instance is bound, and useObjectLabel-identity-5564.test.tsx already pins that through useSafeFieldLabel specifically. The card's settle question is answered rather than assumed: the two derived state maps DO settle, because the effect now runs exactly once per genuine resolver change; the removal additionally restores re-derivation on a language switch, which the ref suppressed. eslint reports no react-hooks/exhaustive-deps finding on the new dep array, so nothing was suppressed to make it pass.",
"tests": "All at b83dd427c (clean tree, git diff HEAD --stat empty), every heavy run through scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. NEW PIN packages/plugin-dashboard/src/__tests__/ObjectPivotTable.i18nResolverDeps.test.tsx (3 cases: derivation count across mount + 3 forced re-renders with NO provider asserting RAW labels; same INSIDE a provider asserting localized labels; language-switch re-derivation) — 'Test Files 1 passed (1) / Tests 3 passed (3)'. PACKAGE SUITES `pnpm exec vitest run packages/plugin-dashboard/ packages/i18n/` from the repo root (never --filter, never a package cwd) — 'Test Files 129 passed (129) / Tests 1607 passed (1607)', lock VERDICT command-exit 0. TYPECHECK `pnpm --filter @object-ui/plugin-dashboard type-check` after `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' build` — VERDICT command-exit 0; output echoed '> tsc --noEmit && tsc -p tsconfig.test.json', so it was not a zero-match silent no-op. LINT repo-wide `pnpm lint` (eslint . --no-inline-config) ran in full under the foreground cap — 0 errors repo-wide (warnings only, all pre-existing no-explicit-any); no narrowing to declare. GATES check-changeset-presence.mjs OK ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check:control-bytes OK (scanned 4870 tracked text files), check:phantom-deps OK, check:self-import OK, check:i18n-keys OK — each quoted from its own printed verdict line, not from a bare $?. REVERSE VERIFICATION: two legs, both on the real commit, each driven by a script carrying trap '<restore>' EXIT INT TERM. NO REBUILD WAS NEEDED AND THAT IS ESTABLISHED, NOT ASSUMED: vitest.config.mts aliases @object-ui/i18n to packages/i18n/src (and @object-ui/plugin-dashboard to its src), so both mutations are live from source rather than through a dist the ablation could silently miss. Each mutation was confirmed ON DISK by grep, never by an editor exit code — the leg-B mutator was a python script that exits non-zero on anchor miss, and the counts were printed both ways. Leg A (restore the ref workaround in ObjectPivotTable.tsx via git checkout of the base rev; on disk: 'fieldLabelRef' 3 occurrences, direct-deps line 0) → RED, \"AssertionError: expected 'Stage (en)' to be 'Stage (ja)'\", 1 failed | 2 passed. Leg B (revert the #5585 memo-stability fix in useObjectLabel.ts; on disk: 'NO_INSTANCE_T;' 0 occurrences, 'const t = boundT;' 1) → RED, 'AssertionError: expected 258 to be 1' — 258 derivations instead of 1, i.e. the unbounded derive loop returns, 1 failed | 2 passed. Restored tree → GREEN 3 passed with git diff HEAD --stat empty. LEGS THAT DO NOT DISCRIMINATE, named: under leg A the two counting cases stay GREEN (the ref version also derives exactly once — that was the point of the ref), so only the language-switch case speaks for the source change; under leg B the two with-provider cases stay GREEN (that identity path was stable before #5585 too), so only the no-provider case speaks for it. Neither leg alone covers the change; the pair does.",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch (triage seat): promoted
finding→pm:queue, Task S. The precondition for the ref workaround is measured gone (#5564 / PR #5585), and the removal shape mirrors #5587 with the template test it names. The card's own two cautions are the acceptance bar: an effect-run-counting pin across re-renders (in and out of a provider), and the settle check on the two derived state maps — a loop here is a stop-and-report, not a force-through. Sequencing: after #5587 merges (its template test is the reference); not hard-blocked.Generated by Claude Code
claude commentedon Aug 23, 2026
Claim —
domain:uiexecution seat. PM round 30, sessionsession_01EuPCi56cnGyykygi3z9w4m, branchclaude/issue-5625-pivottable-i18n-ref-workaround. Default tier (Opus).Triage graded this Task S with the precondition already measured discharged (#5564 / PR #5585) and the removal shape mirroring #5587 plus the template test it names — read that landed diff rather than inferring the shape from this card.
Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
{ "issue": 5625, "status": "done", "branch": "claude/issue-5625-pivottable-i18n-ref-workaround", "pr": "https://github.com/objectstack-ai/objectui/pull/5851", "premise_still_valid": true, "summary": "Removed both i18n-resolver refs (fieldLabelRef / fieldOptionLabelRef and the useEffect keeping them current) from packages/plugin-dashboard/src/ObjectPivotTable.tsx, reading fieldLabel / fieldOptionLabel directly in the metadata-derivation effect and declaring them in its dep array — the shape #5587 landed in ObjectChart (PR #5628), which I read rather than inferring from the card. Precondition re-verified on current main before removing anything: useObjectLabel pins both memo deps to module constants (NO_INSTANCE_T / NO_INSTANCE_I18N) while no i18next instance is bound, and useObjectLabel-identity-5564.test.tsx already pins that through useSafeFieldLabel specifically. The card's settle question is answered rather than assumed: the two derived state maps DO settle, because the effect now runs exactly once per genuine resolver change; the removal additionally restores re-derivation on a language switch, which the ref suppressed. eslint reports no react-hooks/exhaustive-deps finding on the new dep array, so nothing was suppressed to make it pass.", "tests": "All at b83dd427c (clean tree, git diff HEAD --stat empty), every heavy run through scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. NEW PIN packages/plugin-dashboard/src/__tests__/ObjectPivotTable.i18nResolverDeps.test.tsx (3 cases: derivation count across mount + 3 forced re-renders with NO provider asserting RAW labels; same INSIDE a provider asserting localized labels; language-switch re-derivation) — 'Test Files 1 passed (1) / Tests 3 passed (3)'. PACKAGE SUITES `pnpm exec vitest run packages/plugin-dashboard/ packages/i18n/` from the repo root (never --filter, never a package cwd) — 'Test Files 129 passed (129) / Tests 1607 passed (1607)', lock VERDICT command-exit 0. TYPECHECK `pnpm --filter @object-ui/plugin-dashboard type-check` after `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' build` — VERDICT command-exit 0; output echoed '> tsc --noEmit && tsc -p tsconfig.test.json', so it was not a zero-match silent no-op. LINT repo-wide `pnpm lint` (eslint . --no-inline-config) ran in full under the foreground cap — 0 errors repo-wide (warnings only, all pre-existing no-explicit-any); no narrowing to declare. GATES check-changeset-presence.mjs OK ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check:control-bytes OK (scanned 4870 tracked text files), check:phantom-deps OK, check:self-import OK, check:i18n-keys OK — each quoted from its own printed verdict line, not from a bare $?. REVERSE VERIFICATION: two legs, both on the real commit, each driven by a script carrying trap '<restore>' EXIT INT TERM. NO REBUILD WAS NEEDED AND THAT IS ESTABLISHED, NOT ASSUMED: vitest.config.mts aliases @object-ui/i18n to packages/i18n/src (and @object-ui/plugin-dashboard to its src), so both mutations are live from source rather than through a dist the ablation could silently miss. Each mutation was confirmed ON DISK by grep, never by an editor exit code — the leg-B mutator was a python script that exits non-zero on anchor miss, and the counts were printed both ways. Leg A (restore the ref workaround in ObjectPivotTable.tsx via git checkout of the base rev; on disk: 'fieldLabelRef' 3 occurrences, direct-deps line 0) → RED, \"AssertionError: expected 'Stage (en)' to be 'Stage (ja)'\", 1 failed | 2 passed. Leg B (revert the #5585 memo-stability fix in useObjectLabel.ts; on disk: 'NO_INSTANCE_T;' 0 occurrences, 'const t = boundT;' 1) → RED, 'AssertionError: expected 258 to be 1' — 258 derivations instead of 1, i.e. the unbounded derive loop returns, 1 failed | 2 passed. Restored tree → GREEN 3 passed with git diff HEAD --stat empty. LEGS THAT DO NOT DISCRIMINATE, named: under leg A the two counting cases stay GREEN (the ref version also derives exactly once — that was the point of the ref), so only the language-switch case speaks for the source change; under leg B the two with-provider cases stay GREEN (that identity path was stable before #5585 too), so only the no-provider case speaks for it. Neither leg alone covers the change; the pair does.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code