Repository navigation
Commit 14b5597
fix(formula,core,react): a template formatter locale rides a reserved context channel, never a root (#26282)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XGSSiXPYtWQirMK53tWA8Qo)_
Fixes #26232
Clause-②: yes (narrowing)
Metadata-protocol: none
## What changed
The `template` dialect's formatters used to read their locale from
`ctx.extra.locale`, the same bag every caller passes the template's
author-visible roots in. A root named `locale` (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 read `en-US`.
- **`@objectstack/formula`**: `EvalContext.locale?: string` is a
reserved channel, like `timezone`: not bound as a variable, never set by
a root. `templateEngine.evaluate` reads its formatter locale from it
alone (`en-US` when unset or empty). The undeclared `(ctx as { locale
}).locale` read it already had becomes the declared, only rung; the
`extra.locale` rung is gone.
- **`@object-ui/core`**: `fillTemplateHoles(source, roots, locale?)`
passes `locale` on that channel, apart from the roots.
`ActionRunner.setDisplayLocale(read)` installs where the runner reads
the viewer's locale, read per fill; `confirmText` and `successMessage`
format in it, and a `callContext` dispatch (`scopedTo`) keeps it.
- **`@object-ui/react`**: `ActionProvider` and `useActionRunner` install
`useDisplayLocale()` 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 named `locale` was 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}.json` re-recorded (`node
scripts/gen-sdui-manifest-node.mjs`); one changeset (`patch` ×4,
BREAKING banner, ADR-0087 `not-required (no-migration-prescription)`).
## Red first (base `9598405ec`)
Probe through `templateEngine.evaluate` on `origin/main`, source `Set {{
amount | number:2 }} on {{ count }} records, due {{ due | date:long }}`:
```text
no locale root : "Set 1,234,567.50 on 1200 records, due May 6, 2031"
root named locale : "Set 1.234.567,50 on 1200 records, due 6. Mai 2031" (extra.locale = 'de-DE')
ctx.locale zh-CN : "Set 1,234,567.50 on 1200 records, due 2031年5月6日"
```
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 grep` for `templateEngine`, `ExpressionEngine.evaluate`,
`fillTemplateHoles`, `renderTextSlot` and `formatValue` callers at base:
- `ui/packages/core` `fillTemplateHoles` (ActionRunner `confirmMessage`
/ `composeSuccessMessage`, plugin-grid `bulkConfirmText`): roots only,
no locale. Now the runner passes its display locale.
- `service-automation` `renderTextSlot`: flow variables as `extra`,
documented as the locale channel. No engine code sets a flow variable
named `locale` and no example or package flow declares one (`git grep`
of `name: 'locale'` under `examples`, `apps`, `packages/qa`: 0 hits).
Under the ruling it stays a value; flow text formats in `en-US`.
- `packages/formula/src/template-formatters.test.ts`: the one caller
that set the locale via `extra: { locale: 'en-US' }`; now `locale:
'en-US'`.
- Registry callers (`seed-eval`, objectql defaults and formula fields,
plugin-approvals, plugin-sharing, automation conditions) pass no
`locale` in `extra`. `formatValue` (plugin-email) takes `opts.locale`
and is untouched.
## Clause-② from the diff
`yes`: `EvalContext` gains an optional member and `fillTemplateHoles` an
optional argument (public surface grows). `(narrowing)`: a caller that
set the formatter locale through a root (`extra: { locale }`, a flow
variable or bulk param named `locale`) now formats in `en-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 no `locale` root 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 named `locale` prints and does not format
(alone, and beside a context locale); no or empty locale is `en-US`.
`formula` package: 53 files / 1427 tests pass; `typecheck` exit 0.
- `service-automation/src/builtin/text-slot-template.test.ts`: a flow
variable named `locale` prints and formats in `en-US`. Package: 207
files / 2699 tests pass; `typecheck` exit 0.
- `ui/packages/core/.../ActionRunner.copyTemplateLocale-26232.test.ts`:
`fillTemplateHoles` root vs argument; the runner's `confirmText` with
and without a display locale, read per fill, kept across a
`callContext`; `successMessage`. Core: 213 files / 4097 pass, 27
skipped; `type-check` exit 0.
-
`ui/packages/react/.../ActionProvider.copyTemplateLocale-26232.test.tsx`:
real `I18nProvider` + `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-check` exit 0.
- plugin-grid bulk suites (the other `fillTemplateHoles` caller): 27
files / 264 pass.
- Expected strings are computed with `Intl` in each test (no ICU drift
between the container's Node and CI's).
**Ablation** (head `7f9627df`, via `scripts/ablation-replace.mjs`,
formula tests import `./template-engine` from `src`): put back the
`extra.locale` rung ahead of `ctx.locale` → `a root named locale is a
value a hole prints, not the formatter locale` FAILS (1 failed / 10
passed); restored, blob `a2e9acd8fba9` == HEAD, `git diff HEAD` empty.
## 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-test` exit 3 on the shallow clone
(fetched its control `621a4876`, rerun exit 0); `check-sdui-manifest`
exit 1 until the re-record (rerun exit 0); `check:dual-build-cjs-loads`
exit 3 before a full build (rerun after the full build, below). Lint
(`eslint` on 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-closure` and the full dogfood
battery (owed by the `(narrowing)` arm) are in the os-dev report on the
card.
## Acceptance notes
- **Not wired here: the bulk confirm dialog.** `BulkActionDialog` →
`bulkConfirmText` → `fillTemplateHoles` still passes no locale, because
open draft #26217 (#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()` into
`fillTemplateHoles`'s third parameter) once #26217 lands.
- Flow text slots format in `en-US` (one text for every recipient, as
`notify` documents); a per-run locale for them is not this card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XbF1LGGBRTWwTfLay1d51s)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3249e20 commit 14b5597
16 files changed
Lines changed: 385 additions & 20 deletions
File tree
- .changeset
- packages
- formula/src
- services/service-automation/src/builtin
- scripts/sdui-manifest.sources
- ui/packages
- core/src/actions
- __tests__
- react/src
- context
- __tests__
- hooks
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
275 | | - | |
276 | | - | |
277 | | - | |
278 | | - | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
279 | 278 | | |
280 | 279 | | |
281 | 280 | | |
| |||
285 | 284 | | |
286 | 285 | | |
287 | 286 | | |
288 | | - | |
| 287 | + | |
289 | 288 | | |
290 | 289 | | |
291 | 290 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
85 | 98 | | |
86 | 99 | | |
87 | 100 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
106 | | - | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
107 | 109 | | |
108 | 110 | | |
109 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
270 | 277 | | |
271 | 278 | | |
272 | 279 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1283 | 1283 | | |
1284 | 1284 | | |
1285 | 1285 | | |
| 1286 | + | |
1286 | 1287 | | |
1287 | 1288 | | |
1288 | 1289 | | |
| |||
1322 | 1323 | | |
1323 | 1324 | | |
1324 | 1325 | | |
| 1326 | + | |
1325 | 1327 | | |
1326 | 1328 | | |
1327 | 1329 | | |
| |||
1341 | 1343 | | |
1342 | 1344 | | |
1343 | 1345 | | |
| 1346 | + | |
| 1347 | + | |
| 1348 | + | |
| 1349 | + | |
| 1350 | + | |
| 1351 | + | |
| 1352 | + | |
| 1353 | + | |
| 1354 | + | |
| 1355 | + | |
| 1356 | + | |
| 1357 | + | |
| 1358 | + | |
| 1359 | + | |
1344 | 1360 | | |
1345 | 1361 | | |
1346 | 1362 | | |
| |||
1811 | 1827 | | |
1812 | 1828 | | |
1813 | 1829 | | |
1814 | | - | |
| 1830 | + | |
| 1831 | + | |
| 1832 | + | |
| 1833 | + | |
| 1834 | + | |
1815 | 1835 | | |
1816 | 1836 | | |
1817 | 1837 | | |
| |||
1846 | 1866 | | |
1847 | 1867 | | |
1848 | 1868 | | |
1849 | | - | |
| 1869 | + | |
| 1870 | + | |
| 1871 | + | |
| 1872 | + | |
| 1873 | + | |
1850 | 1874 | | |
1851 | 1875 | | |
1852 | 1876 | | |
| |||
2917 | 2941 | | |
2918 | 2942 | | |
2919 | 2943 | | |
2920 | | - | |
2921 | | - | |
2922 | | - | |
| 2944 | + | |
| 2945 | + | |
| 2946 | + | |
2923 | 2947 | | |
2924 | 2948 | | |
2925 | 2949 | | |
| |||
2961 | 2985 | | |
2962 | 2986 | | |
2963 | 2987 | | |
| 2988 | + | |
2964 | 2989 | | |
2965 | 2990 | | |
2966 | 2991 | | |
| |||
0 commit comments