Repository navigation
fix(formula,core,react): a template formatter locale rides a reserved context channel, never a root - #26282
Conversation
…channel, never a root A template's roots are author-visible values: a bulk action's params, a flow's variables. The engine read its formatter locale from extra.locale, so a root named locale set the locale of every formatted value in the sentence, and no viewer locale was ever passed. EvalContext gains a reserved locale field the engine reads instead, the console filler takes the display locale from its caller, and both React runner owners install useDisplayLocale() on the runner. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbF1LGGBRTWwTfLay1d51s
… owners Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbF1LGGBRTWwTfLay1d51s
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbF1LGGBRTWwTfLay1d51s
# Conflicts: # scripts/sdui-manifest.sources/core.json # scripts/sdui-manifest.sources/react.json
…ing main Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbF1LGGBRTWwTfLay1d51s
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3faff829ae941baf49d25565102a545b0195916d && git checkout 3faff829ae941baf49d25565102a545b0195916d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3ed6c86d46f96d15a2a70eab66ee411fe3b58e9 2d2b09be7683ba4bcd1ab278a1c5f420653baac4 && git checkout -B drift-repro b3ed6c86d46f96d15a2a70eab66ee411fe3b58e9 && git merge --no-ff 2d2b09be7683ba4bcd1ab278a1c5f420653baac4
node scripts/docs-audit/affected-docs.mjs --json b3ed6c86d46f96d15a2a70eab66ee411fe3b58e9
|
Contract reviewServed-tier: Inputs read: card #26232 body and all three comments (triage 6422123580, claim 6423821741, os-dev-report 6425204871); PR #26282 body, its 16-file list, and ① Derived judgmentsThe narrowing itself — right. At the base, Every caller of the template dialect, measured in the head tree — all accounted for, right. Runtime producers of a Console wiring — right for the two paths it names. The third Public surface the diff implies, each named right. Widening:
② Semver levelThe changeset ③ Boundary flagsThe os-dev-report (6425204871) lists
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #26232
Clause-②: yes (narrowing)
Metadata-protocol: none
What changed
The
templatedialect's formatters used to read their locale fromctx.extra.locale, the same bag every caller passes the template's author-visible roots in. A root namedlocale(a bulk param, a flow variable) therefore set the formatter locale of the whole sentence, and no caller passed the viewer's locale, so every viewer readen-US.@objectstack/formula:EvalContext.locale?: stringis a reserved channel, liketimezone: not bound as a variable, never set by a root.templateEngine.evaluatereads its formatter locale from it alone (en-USwhen unset or empty). The undeclared(ctx as { locale }).localeread it already had becomes the declared, only rung; theextra.localerung is gone.@object-ui/core:fillTemplateHoles(source, roots, locale?)passeslocaleon that channel, apart from the roots.ActionRunner.setDisplayLocale(read)installs where the runner reads the viewer's locale, read per fill;confirmTextandsuccessMessageformat in it, and acallContextdispatch (scopedTo) keeps it.@object-ui/react:ActionProvideranduseActionRunnerinstalluseDisplayLocale()beside the translator (useActionRunnerDisplayLocale, same latest-ref pattern), so there is no second copy of the locale resolution and a locale switch reaches an already-built runner.@objectstack/service-automation: comment only.renderTextSlot's doc said a flow variable namedlocalewas also the formatters' locale; under the ruling it is a value like any other, so the line now says that.scripts/sdui-manifest.sources/{core,react}.jsonre-recorded (node scripts/gen-sdui-manifest-node.mjs); one changeset (patch×4, BREAKING banner, ADR-0087not-required (no-migration-prescription)).Red first (base
9598405ec)Probe through
templateEngine.evaluateonorigin/main, sourceSet {{ amount | number:2 }} on {{ count }} records, due {{ due | date:long }}:The root sets the formatter locale; and the console filler passed only
{ extra: roots }, so no viewer locale was ever reached.Every caller that passed
extra.locale(measured)git grepfortemplateEngine,ExpressionEngine.evaluate,fillTemplateHoles,renderTextSlotandformatValuecallers at base:ui/packages/corefillTemplateHoles(ActionRunnerconfirmMessage/composeSuccessMessage, plugin-gridbulkConfirmText): roots only, no locale. Now the runner passes its display locale.service-automationrenderTextSlot: flow variables asextra, documented as the locale channel. No engine code sets a flow variable namedlocaleand no example or package flow declares one (git grepofname: 'locale'underexamples,apps,packages/qa: 0 hits). Under the ruling it stays a value; flow text formats inen-US.packages/formula/src/template-formatters.test.ts: the one caller that set the locale viaextra: { locale: 'en-US' }; nowlocale: 'en-US'.seed-eval, objectql defaults and formula fields, plugin-approvals, plugin-sharing, automation conditions) pass nolocaleinextra.formatValue(plugin-email) takesopts.localeand is untouched.Clause-② from the diff
yes:EvalContextgains an optional member andfillTemplateHolesan optional argument (public surface grows).(narrowing): a caller that set the formatter locale through a root (extra: { locale }, a flow variable or bulk param namedlocale) now formats inen-US, and its value prints only where a hole names it. In-repo producers of that: 0 outside the one test above. A template that names nolocaleroot and is evaluated with no locale renders exactly as before (pinned).Tests
packages/formula/src/template-engine.test.ts(new block): the context locale formats; a root namedlocaleprints and does not format (alone, and beside a context locale); no or empty locale isen-US.formulapackage: 53 files / 1427 tests pass;typecheckexit 0.service-automation/src/builtin/text-slot-template.test.ts: a flow variable namedlocaleprints and formats inen-US. Package: 207 files / 2699 tests pass;typecheckexit 0.ui/packages/core/.../ActionRunner.copyTemplateLocale-26232.test.ts:fillTemplateHolesroot vs argument; the runner'sconfirmTextwith and without a display locale, read per fill, kept across acallContext;successMessage. Core: 213 files / 4097 pass, 27 skipped;type-checkexit 0.ui/packages/react/.../ActionProvider.copyTemplateLocale-26232.test.tsx: realI18nProvider+LocalizationProvider+ActionProvider/useActionRunner: tenant locale reaches the toast and the confirm; with no tenant locale the UI language does, and a language switch reaches the same runner. React: 120 files / 1483 pass;type-checkexit 0.fillTemplateHolescaller): 27 files / 264 pass.Intlin each test (no ICU drift between the container's Node and CI's).Ablation (head
7f9627df, viascripts/ablation-replace.mjs, formula tests import./template-enginefromsrc): put back theextra.localerung ahead ofctx.locale→a root named locale is a value a hole prints, not the formatter localeFAILS (1 failed / 10 passed); restored, bloba2e9acd8fba9== HEAD,git diff HEADempty.Gates (local, head
6507217d8)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 72 commands, every one exit 0 except:check-plugin-teardown-shape --self-testexit 3 on the shallow clone (fetched its control621a4876, rerun exit 0);check-sdui-manifestexit 1 until the re-record (rerun exit 0);check:dual-build-cjs-loadsexit 3 before a full build (rerun after the full build, below). Lint (eslinton the 13 touched TS files,--no-inline-config): 0 errors, 29 warnings, the same set the base versions carry. Full build,check:dual-build-cjs-loads,check:dts-closureand the full dogfood battery (owed by the(narrowing)arm) are in the os-dev report on the card.Acceptance notes
BulkActionDialog→bulkConfirmText→fillTemplateHolesstill passes no locale, because open draft fix(plugin-grid): fill a bulk confirmText's param holes with the params' display values #26217 (list bulk actionconfirmText: fill the template slot's declared holes —{{count}}first, the action's params second (the renderer half of objectstack#24798 ruling B) #25942 step 2) rewrites exactly those two files. The hijack is closed for it by the engine change; reaching the viewer's locale there is one argument (useDisplayLocale()intofillTemplateHoles's third parameter) once fix(plugin-grid): fill a bulk confirmText's param holes with the params' display values #26217 lands.en-US(one text for every recipient, asnotifydocuments); a per-run locale for them is not this card.Generated by Claude Code