Repository navigation
Commit 76a1275
feat(formula): label(record.<field>) renders a select value in the reader's locale (#26755)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB77M1hT4XaiEVd31sLrD2dU)_
Fixes #26608
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: #26760
## 维护者速读
- **改了什么**:新增 CEL 函数 `label(record.<field>)`,把 select
字段的存储值渲染为读者语言的选项标签;objectql 在读和写的响应里按请求的 `Accept-Language` 给公式备好标签。`'v'
+ string(record.version_no) + ' · ' + label(record.kind)` 对 zh-CN 读者是
`v2 · 清稿`,对 en-US 读者是 `v2 · Clean`。
- **为什么改**:select 存的是值,公式 nameField 只能拼出 `v2 ·
clean`,保存提示和记录标题对所有语言的读者都显示英文值(#26608)。
- **风险与回滚**:只有调用 `label(` 的公式才会构建标签表,其余读路径只多一次源码扫描。没有新的翻译键,也没有第二条翻译路径(复用
`translateObject`,与 `GET /meta/object` 同一函数)。i18n
服务缺失或抛错时退回声明的标签,读不会失败。回滚即 revert 本 PR。
- **先看**:`packages/objectql/src/formula-option-labels.ts`,然后
`packages/formula/src/stdlib.ts` 里的 `bindOptionLabel`。
- **两分钟验证**:`pnpm --filter @objectstack/objectql exec vitest run
src/engine-formula-option-label.test.ts`,以及 `packages/qa/dogfood` 下的
`test/formula-option-label-locale.dogfood.test.ts`。
- **需要你做的**:本 PR 改了 `skills/**`(Tier H),落地需要你的同意。
## What changes
**`@objectstack/formula`**
- `label(record.<field>)` joins the CEL stdlib and
`CEL_STDLIB_FUNCTIONS`. It is a macro: the type check accepts exactly
`record.<field>` and refuses a literal, a path through a lookup, an
optional select, `previous.<field>` and any computed argument, with a
message that names the one shape.
- It reads data only. `EvalContext.optionLabels` (new, type
`EvalOptionLabels`: field → stored value → label) is resolved by the
caller in the reader's locale before evaluation, so the stdlib holds no
translator or resolver. It is not mounted as a CEL variable.
- An empty value reads `''`, and a value no option declares reads as
itself, as the select control shows it. A field with no options (on
every row, empty or not), a list value (multi-select), and a context
with no `optionLabels` fail loudly.
- The measured catalog counts in `validate.ts` are updated (77
registered names, 42 callable bare, 30 from `registerStdLib`, 38
advertised). The receiver-only count also picks up `hasCapability`,
which was already registered on main and missing from the prose.
**`@objectstack/objectql`**
- New `formula-option-labels.ts` builds the map for one read: the
object's fields that declare `options`, with the i18n service's bundle
laid over them by `translateObject` from `@objectstack/spec/system`. The
bundle and the locale options (`fallbackChain`, `defaultLocale`) are
built as the REST metadata route builds them, so the label a formula
prints is the label the select control shows the same reader:
`objects.<o>.fields.<f>.options.<v>`, then `picklists.<p>.options.<v>`,
then the declared label. No key is spelled in this module.
- `applyFormulaPlan` passes the map to `ExpressionEngine.evaluate`. The
map is built only when some formula in the plan calls `label(`.
- Wired readers, each in `ExecutionContext.locale` (the request's
`Accept-Language`): `find`, `findOne`, the insert and update responses
(`hydrateWriteFormulas`), and `serveFormulaField`.
`expandRelatedRecords` already runs a `find` with the read's context, so
expanded lookups follow.
- Declared labels: stored expressions (`computeStoredExpressions`), the
synchronous `evaluateFormulaField`, any read with no locale, and an
engine with no i18n service or one whose `getTranslations` throws.
- `setI18nService` accepts the optional `getTranslations`,
`getFallbackLocale` and `getDefaultLocale` it now reads. `II18nService`
already carries all three.
**Docs and skill**
- `skills/objectstack-formula/SKILL.md` gains an "Options" row for
`label(record.f)` (+116 tokens, 5627 of 5997). The row says it answers
only in formula fields and faults in a predicate.
- `content/docs/data-modeling/formulas.mdx` gains the stdlib row and a
cookbook entry, "Record name from an option label".
**Skills readings**
| Measure | Before | After |
|:---|---:|---:|
| `skills/objectstack-formula/SKILL.md` lines | 465 | 471 |
| `skills/objectstack-formula/SKILL.md` tokens (ceiling 5997) | 5511 |
5627 |
| Bundle, all `SKILL.md`, lines | 4600 | 4606 |
| Bundle, all `SKILL.md`, tokens (`ceil(bytes / 4)`) | 56017 | 56133 |
| `evals/cel-predicates-formulas.json` tokens (ceiling 1046) | 1046 |
1046 |
## Readers (measured)
| Reader | Where it gets the name | Locale |
|:---|:---|:---|
| Save toast (`recordSaveMessage`, app-shell) | the POST/PATCH response
| writer's `Accept-Language` |
| Record page H1 (`getRecordDisplayName`) | `GET /data/:object/:id` →
`findOne` | reader's |
| Related list | list read → `find` | reader's |
| Lookup display | `expand` → `find` with the same context | reader's |
| REST GET / POST / PATCH | the same engine paths | request's |
| Search | does not index formula fields (`SEARCH_VIRTUAL_TYPES`) |
unchanged |
| Stored computed field (`text` + `expression`) | computed once on write
| declared labels |
| `evaluateFormulaField` (sync, e.g. a hook's record-title accessor) |
evaluated with no read context | declared labels (left out) |
No `ui/**` file changes: the console renders the name the server serves.
The console sends its display language as `Accept-Language` on every
call.
## Tests
- `packages/formula/src/option-label.test.ts` (new): zh-CN and en-US
maps, per-field maps, empty and undeclared values, the run-time faults,
the compile-time refusals, and `inferExpressionType` returning `text`.
- `packages/formula/src/cel-engine.test.ts`: `label` joins the drift
probe.
- `packages/objectql/src/engine-formula-option-label.test.ts` (new): the
insert response in zh-CN and en-US, the update response, `findOne` and
`find` per locale, a base-language tag (`zh`), no locale, a stored
expression keeping the declared label under a zh-CN writer, and no i18n
service or a throwing one.
- `packages/qa/dogfood/test/formula-option-label-locale.dogfood.test.ts`
(new, real boot): POST in zh-CN and en-US, GET by id in both, PATCH in
zh-CN, the list read in zh-CN, and the name matching the option label
`GET /meta/object/:name` serves in the same locale.
## Acceptance notes
- `label()` in a validation rule, predicate or flow condition has no
option labels and faults loudly instead of comparing against a label;
the docs say to compare the stored value there. No publish-time refusal
of `label()` outside a formula is built in this PR.
- The skill eval is owed, as #26760: `cel-predicates-formulas.json` is
at its ceiling (1046 of 1046), and a compact case measures about 130
tokens.
- Search indexing is unchanged.
## Verification
All runs on `f780ce78`, in a cloud worktree, under
`scripts/pm/os-verify-lock.sh`, unless a section names `8ffe3ae6`.
<details><summary>Unit tests: <code>@objectstack/formula</code> and
<code>@objectstack/objectql</code></summary>
- `@objectstack/formula`: `option-label.test.ts`, `cel-engine.test.ts`
and `skill-catalog-sync.test.ts` pass (94 tests), and
`cel-stdlib-drift.test.ts` passes. The catalog counts in `validate.ts`
were measured on the built environment (77 / 42 / 35 / 30 / 38).
- `@objectstack/objectql`: `engine-formula-option-label.test.ts` passes
7/7; with the other `engine-formula*` files, 8 files and 135 tests pass.
</details>
<details><summary>Dogfood on a real boot</summary>
- `@objectstack/dogfood`: `formula-option-label-locale.dogfood.test.ts`
passes 5/5.
</details>
<details><summary>Typecheck</summary>
- `typecheck` passes for `@objectstack/formula` and
`@objectstack/objectql` (including `check:test-typecheck`).
- `tsc --noEmit` passes for `@objectstack/dogfood` with
`NODE_OPTIONS=--max-old-space-size=8192`; at the default heap it runs
out of memory locally, before reporting anything.
</details>
<details><summary>Derived gates: 102 commands, 101 measured green, 1 not
measured</summary>
- `node scripts/pm/dispatch-gates.mjs --commands` derives 102 commands;
99 exit 0 on the first run.
- `check:objectql-double-limit` found that the new test's `find` double
ignored the caller's `limit`. Fixed in `f780ce78`, and it now reports
OK.
- `spec check:skill-examples` exited 3 because the client SDK was not
built. After building `@objectstack/client-react` and its dependencies
it passes: 266 examples type-check.
- `check:dual-build-cjs-loads` exits 3 because it needs a whole-repo
build; nothing was measured. That is left to CI.
- `--ran` with exit codes: 102 derived families accounted for, 101 run,
1 NOT-MEASURED (the recorded exit 3).
</details>
<details><summary>Contract-review should-fixes on
<code>8ffe3ae6</code></summary>
- `optionLabelOf` checks the field's options before the empty-value
answer; `option-label.test.ts` pins a null text field faulting.
- `@objectstack/formula`: `option-label`, `cel-engine`,
`skill-catalog-sync` and `cel-stdlib-drift` pass (4 files, 98 tests);
`typecheck` passes. `@objectstack/objectql`:
`engine-formula-option-label.test.ts` passes 7/7 against the rebuilt
formula package.
- Exit 0: `spec check:skill-docs`, `check:skill-identifier-liveness`,
`check:skill-compatibility`, `check:skill-frame-sync`,
`check:skill-top-level-keys`, `check:nul-bytes`,
`check-skills-token-ratchet`, `spec check:skill-examples`.
</details>
<details><summary>Skills token ratchet</summary>
- `check-skills-token-ratchet` exits 0. The readings are in the Skills
readings table above.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzfay61tE4MHpZNiV4t
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e438a55 commit 76a1275
14 files changed
Lines changed: 919 additions & 29 deletions
File tree
- .changeset
- content/docs/data-modeling
- packages
- formula/src
- objectql/src
- qa/dogfood/test
- skills/objectstack-formula
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
| 199 | + | |
199 | 200 | | |
200 | 201 | | |
201 | 202 | | |
| |||
265 | 266 | | |
266 | 267 | | |
267 | 268 | | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
268 | 303 | | |
269 | 304 | | |
270 | 305 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
511 | 511 | | |
512 | 512 | | |
513 | 513 | | |
514 | | - | |
| 514 | + | |
515 | 515 | | |
516 | 516 | | |
517 | 517 | | |
518 | 518 | | |
519 | 519 | | |
520 | | - | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
521 | 523 | | |
522 | 524 | | |
523 | 525 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
| 86 | + | |
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
| |||
121 | 122 | | |
122 | 123 | | |
123 | 124 | | |
124 | | - | |
125 | | - | |
| 125 | + | |
| 126 | + | |
126 | 127 | | |
127 | 128 | | |
128 | 129 | | |
| |||
2046 | 2047 | | |
2047 | 2048 | | |
2048 | 2049 | | |
| 2050 | + | |
2049 | 2051 | | |
2050 | 2052 | | |
2051 | 2053 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
173 | | - | |
| 173 | + | |
| 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 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 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 | + | |
0 commit comments