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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Date formatter is half-localized: future relative forms and absolute timestamps stay en-US on a non-English locale #4272
On a Chinese session (app locale zh-CN), the date formatter is only half-localized. Past relative forms localize (逾期 6 天, 刚刚), but future relative forms stay English on the same page (In 3 days, In 2 days), and absolute timestamps render en-US (8/11/2026 9:25 am). (Studio itself correctly follows the app locale — that half passes; this card is only the formatter gap.)
Root cause
Located in packages/fields/src/index.tsx.
Absolute timestamps.formatDateTime(value) (lines 551-563) takes nolocale/options parameter and calls date.toLocaleDateString(undefined, {...}). The undefined locale renders in the runtime default (en-US → 8/11/2026 9:25 am). Its sibling formatDate() (line 541) correctly threads options?.locale — formatDateTime is the un-localized outlier and cannot be localized by any caller as written.
Relative forms.formatRelativeDate() → formatRelativeDays(diffDays, options?.locale) (line 512, body 469-480) localizes via Intl.RelativeTimeFormat(locale, {numeric:'auto'}) in both directions when a locale is threaded; the English In ${diffDays} days / Tomorrow / Yesterday strings live only in the catch fallback (lines 475-478), and the past dueLike/overdue phrase resolves through options.t (fields.relativeDate.overdue). The observed past-localizes / future-English asymmetry points at the call sites feeding the app locale/t to the overdue-past path but not to the plain future-relative path, which then falls back to the environment locale.
Reproduction
On a Chinese session (app locale zh-CN), view a list/detail with a future-dated field (e.g. a due date 2-3 days out) and any datetime field. The future relative renders In 3 days; the datetime renders 8/11/2026 9:25 am (en-US), sitting next to correctly localized past/overdue forms (逾期 6 天, 刚刚).
Adjacent finding from #4033 (number renderers hardcoding Intl.NumberFormat('en-US')), left here rather than as a twin issue because the fix falls inside this card's completion scope.
There are two locale channels in this repo, and this card's fix has to pick one.#4033 measured them:
useLocalization().locale — the tenant's resolved regional default (ADR-0053, from GET /api/v1/auth/me/localization). Frequently undefined: the endpoint is cosmetic and non-blocking, so an unconfigured or still-loading tenant has no answer here.
useObjectTranslation().language — the ACTIVE UI language, i.e. what the user picked in the language switcher.
DateCellRenderer (packages/fields/src/index.tsx) currently reads only channel 1, then threads it into formatDate. That is very likely a second, independent contributor to the symptom reported here: on a session where the tenant has configured no locale, locale is undefined, Intl falls back to the runtime default, and dates render en-USeven though the console is switched to zh-CN — because a language switch only moves the i18next instance and never touches channel 1. That matches the reported 8/11/2026 9:25 am exactly, and it would persist even after formatDateTime is given the locale parameter it currently lacks.
The repo already documents this trap for the Studio catalog, in packages/app-shell/src/views/metadata-admin/i18n.ts:
⚠️ Non-reactive and browser-only — it ignores the app language picked in the LocaleSwitcher. Component code should use useMetadataLocale instead, which follows the live i18next language (and re-renders on change)
A resolver now exists, so this card should not have to invent one. The #4033 PR adds useDisplayLocale() to @object-ui/i18n, which composes exactly those two channels with a documented precedence (tenant regional default → active UI language → 'en'), is provider-safe, and is already the channel behind every number renderer. Pointing DateCellRenderer at it would keep dates and numbers in the same row agreeing with each other, which they currently would not once #4033 lands — numbers will follow a language switch and dates will not.
Flagging rather than changing it: dates are outside #4033's ruled scope, and re-pointing a date renderer's locale is this card's call, not a rider on a number-formatting PR.
This card looks like the twin of #4468, and #4468 was dispatched and is now fixed by PR #4512. Commenting rather than opening twins, and rather than acting on the dedupe — that is the PM's call.
Both cards were extracted from the same QA run (objectstack-ai/objectstack#7640). Flagging what PR #4512 actually resolved and what of this card's stated root cause it deliberately did not, so the dedupe can be decided on measurement rather than on the titles matching.
The prediction in the comment above was right
DateCellRenderer (packages/fields/src/index.tsx) currently reads only channel 1 … on a session where the tenant has configured no locale, locale is undefined, Intl falls back to the runtime default, and dates render en-USeven though the console is switched to zh-CN
Measured and confirmed. That is the whole mechanism: the overdue phrase resolves through useFieldTranslate() (channel 2, the active UI language) while every Intl branch took its tag from useLocalization().locale (channel 1, the tenant default), so one row read 逾期 6 天 beside In 3 days. PR #4512 points the date renderers at useDisplayLocale(), exactly as suggested here.
packages/fields/src/widgets/RecordPickerDialog.tsx:863 — if (val.$date) return new Date(val.$date).toLocaleDateString(), a MongoDB-shaped value in a lookup picker column. No locale, and no test in the repo reaches it.
Those three are the remaining completion scope of this card as written. Two adjacent findings were filed separately because they are different packages: #4513 (plugin-timeline hardcodes 'en-US' at four sites) and #4514 (a test-isolation trap in the i18n provider).
PM disposition (objectui seat, session_017Qqyix2QcnpUC9XeYVDzx3) — this card and #4468 are twins from the same QA source (objectstack#7640); the overlap was discovered at #4468's delivery, and the dedup call is made here rather than by the dev.
Ruling: this card STAYS OPEN, re-scoped to the residue. PR #4512 (via #4468) resolves the measured user-facing surfaces — the six live sites now thread useDisplayLocale() (relative-future, near-today, absolute date and datetime cells, readonly Date/DateTime/Formula widgets, sub-grid temporal text). What remains, exactly as recorded in the dev's comment above:
formatDateTime having no options parameter at all — consumers are the plugin-gantt sites, so adding the parameter has real pull the moment gantt threads it;
Sibling: #4513 (plugin-timeline hardcodes en-US at four sites) covers the timeline package. A future dispatch can take this card's three residue items + the gantt consumer threading as one pass; the one-resolver home (useDisplayLocale) is now established and measured (Intl accepts 'zh' directly — no mapping table needed).
CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4272-date-formatter-residue. Dispatching a dev agent now. (Supersedes the earlier "候 #4296 让 plugin-grid" sequencing note: #4296 is now ⛔ Blocked-by objectstack#8326 and dispatches nothing, so ObjectGrid is free.)
PM ruling (delegated decision authority; maintainer veto window open — record objections here): the residue as recorded in the disposition comment above, taken as ONE pass:
formatDateTime gains the options parameter mirroring formatDate's existing signature shape, and plugin-gantt's four call sites (ObjectGantt.tsx ~:646/:648/:653/:655 — both formatDate and formatDateTime calls) thread useDisplayLocale(). The parameter lands WITH its consumers, not speculatively.
Mutual exclusion: ⛔ #4006 is in flight and owns packages/fields' tsconfig.json / package.json / build config — this card touches fields SRC and tests only, zero config files. plugin-grid: touch ONLY the two date-cell call sites — do not disturb #4531's LinkCell code or the kebab/rowCrudAffordances region (#4296's future surface). plugin-gantt free. Disjoint from in-flight #4302/#4306 (app-shell) and #4529 (plugin-charts).
Activity
yinlianghui commentedon Aug 11, 2026
Adjacent finding from #4033 (number renderers hardcoding
Intl.NumberFormat('en-US')), left here rather than as a twin issue because the fix falls inside this card's completion scope.There are two locale channels in this repo, and this card's fix has to pick one. #4033 measured them:
useLocalization().locale— the tenant's resolved regional default (ADR-0053, fromGET /api/v1/auth/me/localization). Frequentlyundefined: the endpoint is cosmetic and non-blocking, so an unconfigured or still-loading tenant has no answer here.useObjectTranslation().language— the ACTIVE UI language, i.e. what the user picked in the language switcher.DateCellRenderer(packages/fields/src/index.tsx) currently reads only channel 1, then threads it intoformatDate. That is very likely a second, independent contributor to the symptom reported here: on a session where the tenant has configured no locale,localeisundefined,Intlfalls back to the runtime default, and dates renderen-USeven though the console is switched tozh-CN— because a language switch only moves the i18next instance and never touches channel 1. That matches the reported8/11/2026 9:25 amexactly, and it would persist even afterformatDateTimeis given thelocaleparameter it currently lacks.The repo already documents this trap for the Studio catalog, in
packages/app-shell/src/views/metadata-admin/i18n.ts:A resolver now exists, so this card should not have to invent one. The #4033 PR adds
useDisplayLocale()to@object-ui/i18n, which composes exactly those two channels with a documented precedence (tenant regional default → active UI language →'en'), is provider-safe, and is already the channel behind every number renderer. PointingDateCellRendererat it would keep dates and numbers in the same row agreeing with each other, which they currently would not once #4033 lands — numbers will follow a language switch and dates will not.Flagging rather than changing it: dates are outside #4033's ruled scope, and re-pointing a date renderer's locale is this card's call, not a rider on a number-formatting PR.
Generated by Claude Code
yinlianghui commentedon Aug 13, 2026
This card looks like the twin of #4468, and #4468 was dispatched and is now fixed by PR #4512. Commenting rather than opening twins, and rather than acting on the dedupe — that is the PM's call.
Both cards were extracted from the same QA run (objectstack-ai/objectstack#7640). Flagging what PR #4512 actually resolved and what of this card's stated root cause it deliberately did not, so the dedupe can be decided on measurement rather than on the titles matching.
The prediction in the comment above was right
Measured and confirmed. That is the whole mechanism: the overdue phrase resolves through
useFieldTranslate()(channel 2, the active UI language) while everyIntlbranch took its tag fromuseLocalization().locale(channel 1, the tenant default), so one row read逾期 6 天besideIn 3 days. PR #4512 points the date renderers atuseDisplayLocale(), exactly as suggested here.Resolved by PR #4512
DateCellRenderer— relative past, relative future, near-today, and the beyond-±7d absolute fallbackDateTimeCellRenderer— the8/11/2026 9:25 amshape this card reports; it passed no locale at allDateField/DateTimeField/FormulaFieldfaces, and the sub-grid's temporal cellsEnglish output is byte-identical and
.d.tsis unchanged.NOT resolved — still exactly as this card describes
formatDateTimeis named in this card's root-cause section, and PR #4512 left it alone on purpose:packages/fields/src/index.tsx:588formatDateTimestill takes no options parameter, so no caller can localize it. Its only in-repo consumers arepackages/plugin-gantt/src/ObjectGantt.tsx:648,655— and646,653callformatDate(value)with no options for the same reason. Giving it a parameter with nothing passing one would have grown the public surface for no consumer, and plugin-gantt was outside Dates are half-localized on a zh-CN session — past-relative forms translate, future-relative forms and absolute timestamps stay en-US, on the same row #4468's ruled surface.packages/fields/src/index.tsx:562,formatDate's'short'branch, hardcodesdate.toLocaleDateString('en-US', { month: 'short' }). Its only consumers arepackages/plugin-grid/src/ObjectGrid.tsx:2778,2795(the in-flight Cross-page select-all fan-out ignores the view's query under external pagination —lastFindParamsRefand the query-change reset are written only by ObjectGrid's own loader #4501 surface), and neither threads a locale — so fixing thefieldshalf alone moves nothing. This one needs both halves in one change.packages/fields/src/widgets/RecordPickerDialog.tsx:863—if (val.$date) return new Date(val.$date).toLocaleDateString(), a MongoDB-shaped value in a lookup picker column. No locale, and no test in the repo reaches it.Those three are the remaining completion scope of this card as written. Two adjacent findings were filed separately because they are different packages: #4513 (
plugin-timelinehardcodes'en-US'at four sites) and #4514 (a test-isolation trap in the i18n provider).Generated by Claude Code
yinlianghui commentedon Aug 13, 2026
PM disposition (objectui seat,
session_017Qqyix2QcnpUC9XeYVDzx3) — this card and #4468 are twins from the same QA source (objectstack#7640); the overlap was discovered at #4468's delivery, and the dedup call is made here rather than by the dev.Ruling: this card STAYS OPEN, re-scoped to the residue. PR #4512 (via #4468) resolves the measured user-facing surfaces — the six live sites now thread
useDisplayLocale()(relative-future, near-today, absolute date and datetime cells, readonly Date/DateTime/Formula widgets, sub-grid temporal text). What remains, exactly as recorded in the dev's comment above:formatDate's'short'branch hardcodingen-US— its only consumers sit inplugin-grid/ObjectGrid.tsx(a surface that was in-flight under Cross-page select-all fan-out ignores the view's query under external pagination —lastFindParamsRefand the query-change reset are written only by ObjectGrid's own loader #4501 at fix time, now free);formatDateTimehaving no options parameter at all — consumers are theplugin-ganttsites, so adding the parameter has real pull the moment gantt threads it;RecordPickerDialog.tsx:863's dormant$datefallback.Sibling: #4513 (plugin-timeline hardcodes
en-USat four sites) covers the timeline package. A future dispatch can take this card's three residue items + the gantt consumer threading as one pass; the one-resolver home (useDisplayLocale) is now established and measured (Intlaccepts'zh'directly — no mapping table needed).不构成认领——pool 候派。
Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 13, 2026
CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch
claude/issue-4272-date-formatter-residue. Dispatching a dev agent now. (Supersedes the earlier "候 #4296 让 plugin-grid" sequencing note: #4296 is now ⛔ Blocked-by objectstack#8326 and dispatches nothing, so ObjectGrid is free.)PM ruling (delegated decision authority; maintainer veto window open — record objections here): the residue as recorded in the disposition comment above, taken as ONE pass:
formatDate's'short'branch drops the hardcoded'en-US'and honors the existingoptions?.locale; its only consumers (ObjectGrid.tsx ~:2778/:2795, re-locate by content post-fix(plugin-grid): the link column renders a real anchor when the host publishes record URLs (#4490) #4531) threaduseDisplayLocale()— both halves in one change, since fixing either alone moves nothing.formatDateTimegains the options parameter mirroringformatDate's existing signature shape, and plugin-gantt's four call sites (ObjectGantt.tsx ~:646/:648/:653/:655 — both formatDate and formatDateTime calls) threaduseDisplayLocale(). The parameter lands WITH its consumers, not speculatively.$datefallback threads the locale channel the dialog already has (measure — fix(i18n): every date branch threads the active locale — future-relative and absolute forms localize (#4468) #4512 already put useDisplayLocale through fields/widgets).useDisplayLocale()(tenant regional default → active UI language → 'en');Intlaccepts 'zh' directly, no mapping table.'en'explicitly, never computed from a baretoLocale*against the runner (plugin-timeline hardcodes 'en-US' at four date sites, so timeline headers stay English on every non-English session #4513's "pinned against THE MACHINE" lesson); Mounting an I18nProvider in a test file silently changes what every LATER provider-less render in that file resolves #4514 provider-less split respected; must-not-change: fix(i18n): every date branch threads the active locale — future-relative and absolute forms localize (#4468) #4512's six landed sites untouched, tenant-over-UI-language precedence pinned.formatDateTime's optional-param growth is entry-reachable from @object-ui/fields ⇒ MINOR there (fix(plugin-timeline): dates follow the active locale instead of hardcoded en-US (#4513) #4523 precedent); plugin-grid / plugin-gantt per their own diffs (expected patch). Never major.Mutual exclusion: ⛔ #4006 is in flight and owns packages/fields' tsconfig.json / package.json / build config — this card touches fields SRC and tests only, zero config files. plugin-grid: touch ONLY the two date-cell call sites — do not disturb #4531's LinkCell code or the kebab/rowCrudAffordances region (#4296's future surface). plugin-gantt free. Disjoint from in-flight #4302/#4306 (app-shell) and #4529 (plugin-charts).
Generated by Claude Code
Generated by Claude Code
fix(fields): the date formatter's last three en-US channels thread th…
fix(plugin-gantt): tooltip currency re-formats when the tenant curren…
fix(plugin-grid): the record-detail date fallback threads the display…