fix(i18n): every date branch threads the active locale — future-relative and absolute forms localize (#4468) - #4512
Merged
Conversation
Date rendering had two locale channels and only one followed the user's language: the overdue phrase resolved through the translate fn (active UI language) while every Intl branch read the raw tenant locale, which is undefined on an unconfigured workspace — and undefined means the MACHINE's locale. So one row showed 逾期 6 天 beside In 3 days, and datetime columns rendered 8/11/2026 12:00 am on a zh session. Every date-formatting site in @object-ui/fields now resolves through the existing useDisplayLocale() channel (tenant locale -> active UI language -> en): DateCellRenderer, DateTimeCellRenderer, the read-only DateField / DateTimeField / FormulaField faces, and the sub-grid's temporal cells. English output is byte-identical and the overdue wording is untouched. No public signature changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Aug 13, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 13, 2026 03:12
This was referenced Aug 13, 2026
Merged
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4468
On a
zhsession the same task row rendered逾期 6 天in one column andIn 3 daysin the next, with the datetime column reading8/11/2026 12:00 am. Not a missing translation — a second locale channel that nobody was following.1. Census — every date-formatting site the affected surfaces reach
The console list / all-views pages render date columns through
getCellRenderer('date' | 'datetime')in@object-ui/fields; the same records' detail and form faces go through the read-only field widgets. Column 3 is the locale each site threaded before this PR.packages/fields/src/index.tsx:824DateCellRendereruseLocalization().locale— the tenant default only,undefinedon an unconfigured workspacepackages/fields/src/index.tsx:866DateTimeCellRendererundefinedpackages/fields/src/widgets/DateField.tsx:17(readonly)packages/fields/src/widgets/DateTimeField.tsx:17(readonly)packages/fields/src/widgets/FormulaField.tsx:14(return_type: 'date')packages/fields/src/widgets/GridField.tsx:400sub-gridtemporalText/displayTextpackages/fields/src/index.tsx:506formatRelativeDaysoptions.locale— already correctpackages/fields/src/index.tsx:555formatDatedefault branchoptions.locale— already correctfields.relativeDate.overdueoptions.t→ the ACTIVE UI languagepackages/fields/src/index.tsx:562formatDate'short'branch'en-US'packages/fields/src/index.tsx:588formatDateTimepackages/fields/src/widgets/RecordPickerDialog.tsx:863$datefallbackThe mechanism the census exposes
There are two locale channels, and each half of a row was reading a different one:
useFieldTranslate()→ the active UI language →逾期 6 天;Intlbranch took its tag fromuseLocalization().locale— the tenant's regional default (ADR-0053), which isundefinedon any workspace that never configured one.Intlreadsundefinedas the machine's locale, so those branches rendered en-US beside the Chinese one. A language switch moves the i18next instance and never touches that channel, which is why switching to Chinese fixed the labels and not the dates.2. One resolver
useDisplayLocale()in@object-ui/i18nalready composes both channels with a documented precedence — tenant regional default → active UI language →'en'— and is already what every number and currency renderer uses. Every site above now resolves through it, so a date and the number beside it can no longer disagree.Measured before adopting it, as the ruling asked:
'zh'↔ BCP-47 mapping lives in exactly one place, and that place is this hook — because no mapping is needed. The UI language codes this renderer ships ('zh','ja','de', …) are already well-formed BCP-47 language subtags, soIntlaccepts them verbatim:Intl.RelativeTimeFormat('zh', { numeric: 'auto' }).format(3, 'day')is3天后, identical to what'zh-CN'produces. There is no table to centralize and none was added. The card's note that'zh-CN'is rejected byisKnownLanguage()is about the language channel's input, not about whatIntlis handed.formatRelativeDayswas alreadyIntl.RelativeTimeFormat; the overdue phrase was alreadyt(). Neither moved. The change is entirely in which tag reaches the first one.useDisplayLocale()is a hook, so each of the six sites had to be a component that could call it — all six are. No call site moved between modules, no exported signature changed, and the two module-private helpers that gained alocaleparameter (temporalText,displayText) have three call sites, all insideGridFielditself.GridField.test.tsxpinned its temporal expectations with baretoLocaleDateString()/toLocaleTimeString()— i.e. against the machine. They agreed with the widget only because the runner is en-US, and they would have kept agreeing after the widget started following the session, which is precisely the bug they now need to be able to see. They are spelled'en'now — the valueuseDisplayLocale()resolves to with no provider — so the pin states what it means. No other existing expectation changed.3. Red-first, per measured branch
Captured verbatim from the run against unmodified renderers, then re-measured after. Every
zhcase below was red before and green after; the reverse verification (reverting the five source files ontoorigin/mainwhile keeping the tests) turns exactly these eight red again and nothing else.In 3 days3天后6 days ago6天前Today今天Sep 129月12日8/11/2026+12:00 am2026/8/11+上午12:00DateField8/11/20262026/8/116/17/20262026/6/178/11/20262026/8/114. Must-not-change — green on both sides
In 3 days,6 days ago,Today,Tomorrow,Yesterday,8/11/2026,12:00 am— asserted as literals, and green before and after.'en'and the runner'sen-USagree on every one of these forms, which is why this is a pin rather than a hope.逾期 6 天(zh) andOverdue 6d(en) still resolve throught(), not throughIntl, and the overdue cell keeps itstext-red-600.zhchrome + tenanten→In 3 days)..d.tsis byte-identical.packages/fields/dist/index.d.tshashesf69dd0821892347bc4f8d69951ed587580046ec5ae043f1f0b8cb960853cb54fboth before and after — no public surface grew, hencepatchand notminor.@object-ui/i18n's diff is comment-only (a docstring inuseDisplayLocaleclaimedDateCellRenderer"already formats from this channel" — the one thing that was not true).One test expectation I wrote had to be withdrawn rather than made to pass, and it is worth recording: a provider-less case asserting the
'en'last resort cannot live in the new file.useObjectTranslation()outside a provider reportsi18n.languagefrom react-i18next's global instance, and everyI18nProvidermounted above leaves that global on the language it was given — so a provider-less render placed after thezhcases resolveszh, and the assertion would have been about test ordering, not about the fallback. That pin lives inDateCellRenderer.test.tsx, which mounts no provider at all, and both files now say so. Filed as a repo-wide trap in the findings below.5. Census sites deliberately NOT fixed here
formatDate(…, 'short')'s hardcoded'en-US'— its only consumers areplugin-grid/src/ObjectGrid.tsx:2778,2795, which is 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 call site threads a locale at all. Changing thefieldshalf alone would move nothing for any current caller; the fix has to land on both halves at once.formatDateTime— takes no options parameter, so no caller can localize it. Its only in-repo consumers areplugin-gantt/src/ObjectGantt.tsx:648,655(and646,653forformatDate), which is neither a measured surface nor covered by this card's scope; adding a parameter here with nothing passing it would grow the public surface for no consumer.RecordPickerDialog's$datefallback — a MongoDB-shaped value in a lookup picker column, reachable by no test in the repo.All three are recorded on #4272 (see below) rather than fixed on a rider.
6. Note for triage — #4272 looks like this card's twin
#4272 is open,
pm:queue, undispatched, and describes the same defect from the same QA run (objectstack-ai/objectstack#7640). Its comment thread independently predicted the exact root cause measured here —DateCellRendererreading channel 1 only,useDisplayLocale()as the resolver. This PR does not close it: #4272's stated root cause also namesformatDateTime's missing parameter, which is site 11 above and is left alone on purpose. Flagging for the PM to dedupe rather than acting on it.Verification
Reverse verification:
git checkout origin/main --the five source files, tests kept →8 failed | 1340 passed, the eight being exactly thezhrows in §3.Generated by Claude Code