Repository navigation
Commit c27404f
Fixes #21006
Clause-②: no
## What changed
`translateObject` (`packages/spec/src/system/i18n-resolver.ts`) now
translates each view in an object document's embedded `listViews`. It
overlays the view's `label`, its `description` and its
`bulkActionDefs[]` copy (def `label` / `confirmText` / `confirmLabel`,
param `label` / `help` / `placeholder`). These are the same strings
`translateView` overlays on a served view document.
- **One key shape.** The address is `objects.OBJECT._views.VIEW.*`,
where VIEW is the record key under `listViews`. `os i18n extract` writes
exactly that: its `obj.listViews` loop hands `pushViewEntries` the
record key (`packages/cli/src/utils/i18n-extract.ts`). The record key is
already bare, so `viewTranslationKey`, which strips a served view's
`OBJECT.` prefix, is not involved, and the view's own optional `name` is
never read. `resolveViewLabel` / `resolveViewDescription` and the
embedded channel now share one private lookup (`lookupViewText`) of
`_views.VIEW.label` / `.description`. No second key function exists.
- **Bulk-action copy** goes through the existing
`translateBulkActionDefs`, reading `view.bulkActionDefs`. That is the
authored address the extractor reads. A served view document nests the
same list under `config`, and `translateView` keeps reading it there.
- **Override order** follows ADR-0029 D9.2a, as `translateView` applies
it (the rule from #20680 / #20730). Each string is judged by the
existing `valueOverridesPackagedBase` against the same view in the
packaged object's `listViews`. That packaged object is the
`packagedBase` the REST read gate already supplies for objects
(`getPackagedObjectBase`). A view the packaged base does not carry
counts as authored. With no base supplied, the catalog applies.
- **Not changed:** `emptyState` is not translated server-side on either
channel. `translateView` never translated it, and the extractor's
docblock names the client as its only reader.
- `ObjectLike` gains an optional `listViews` member, typed as a
string-keyed `Record` of `unknown`. It is a TS-only input shape: no
export is added and no generated artefact moves. The only code that
builds an `ObjectLike` in this repo (service-analytics' `{ name, fields
}`) still type-checks. The reverse verification is below.
The change lands where the claim said it would. `translateObject`
produces the served labels, and the extractor is untouched.
## Measured
**H1, the key shape.** An embedded `listViews` entry is an
`ObjectListViewSchema` (`data/object.zod.ts`). Its copy-bearing keys
that the extractor emits under `_views.VIEW` are `label`, `description`,
`emptyState.title` / `.message` and `bulkActionDefs[].*`. All of them
sit under the key shape the resolver already reads. Nothing needed a new
key.
**H2, the override order.** `translateView` and `translateDashboard`
both judge with `valueOverridesPackagedBase` against `packagedBase`
(#20731 / #20680). The embedded channel uses the same predicate and the
same three edges: no base means the catalog applies, a non-string label
is never an override, and an edit back to the shipped string is a no-op.
**H3, the door.** `GET /api/v1/meta/object` goes
`meta-item-read-gate.ts` `translateMetaDocument` / `translateMetaList`,
then `translateMetadataDocument('object')`, then `translateObject`, with
`packagedBase` from `getPackagedObjectBase`.
`metadata-protocol/src/protocol.ts` names `translateObject` only in a
docblock and does not call it. I measured the door once through the REST
route handlers. The protocol double served the REAL `SysAccount`
(`@objectstack/platform-objects/identity`), `getPackagedObjectBase`
answered the same object, and the i18n service served the REAL shipped
`en` / `zhCN` bundles (`@objectstack/platform-objects/apps`). It ran as
a scratch test that is not committed.
| read (zh-CN unless noted) | before (spec dist at `2f2fa11d7`) | after
(spec dist at this branch) |
|:--|:--|:--|
| item `label` (object) | `身份链接` | `身份链接` |
| item `listViews.mine.label` | `My Links` | `我的链接` |
| item `listViews.by_provider.label` | `By Provider` | `按提供方` |
| list `listViews.mine.label` | `My Links` | `我的链接` |
| org overlay `mine` = `My Links (edited-21006)`, item | `My Links
(edited-21006)` | `My Links (edited-21006)` (the overlay wins) |
| same overlay, sibling `by_provider` | `By Provider` | `按提供方` |
| `en`, item `listViews.mine.label` | `My Links` | `My Links` |
The CLI's coverage still counts the same keys. `i18n-coverage` takes its
key set from the extractor's `collectExpectedEntries`, which this PR
does not touch. `pnpm check:i18n-coverage` exited 0 and the CLI's i18n
unit tests passed (below), so extract and serve now agree.
## Tests
New file
`packages/spec/src/system/i18n-resolver.object-list-views.test.ts`, 21
cases. The `sys_account` fixture is transcribed from
`identity/sys-account.object.ts`, with its `_views` leaves taken
verbatim from the shipped `en` / `zh-CN` bundles. Every view is parsed
by `ObjectListViewSchema` and the bundle by `TranslationBundleSchema`.
- `sys_account`'s `mine` tab is served `我的链接` in zh-CN, and every
sibling tab is translated too.
- **Controls:** a view with no catalog entry keeps its authored label;
an object with no `_views` gets the same `listViews` reference back; no
bundle gets the same reference back; an object with no `listViews` gains
none.
- The view is keyed by its record key, never by its `name`. BCP-47
base-language matching applies, and a default-locale request does not
walk the fallback chain.
- `description` and bulk-action copy are translated from the embedded
view's own address.
- **Override order:** an edited label wins in zh-CN and `en`, each tab
is judged separately, no base means the catalog applies, a reset
restores the translation, and the edge cases hold (edit back to the
shipped string, inline map). A tab the base does not ship keeps its
label. The object-scalar rule and the tab rule are independent.
- Neither input is mutated, and `translateMetadataDocument('object')`
reaches the change.
**Reverse verification.** The fix was committed first. Each mutation
went through `scripts/ablation-replace.mjs`, which reported the anchor
hit, the blob moved, and then restored to blob == HEAD with `git diff
HEAD` empty.
- Leg 1: replace the `translateObjectListViews(...)` call with
`doc.listViews`. Result: **15 failed / 6 passed** of 21. The 6 that
passed are the controls and the edited-wins cases, which pass when
nothing is translated.
- Leg 2: replace the per-view `valueOverridesPackagedBase(...)` with
`false`. Result: **5 failed / 16 passed**. All 5 failures are override
cases.
- Cross-package type: inject `listViews: 5` into service-analytics'
`ObjectLike` literal. `tsc` answered `src/plugin.ts(1056,61): error
TS2322`, saying type `number` is not assignable to the string-keyed
`Record` of `unknown`. That is the rebuilt `.d.ts`; the old one would
have answered an excess-property error. Restored.
## Local verification (HEAD `402ab32f5`)
The spec closure (①) is empty, because `packages/spec` has no workspace
dependencies. Consumer closures were built with `turbo run build
--filter='@objectstack/rest^...'` and with the `@objectstack/cli^...` /
`@objectstack/plugin-approvals^...` packages that were not built yet.
- `pnpm --filter @objectstack/spec build` → 0. `check:generated` → `✓
All 15 generated artifacts are up to date`.
- `pnpm --filter @objectstack/spec typecheck` → 0
(`check:test-typecheck: OK`). `vitest run src/system` → 49 files / 1720
passed.
- Consumers, one reading per package:
- `@objectstack/rest`, the five `meta-*i18n*` tests: 5 files / 96
passed.
- `@objectstack/metadata-protocol`, full suite: 195 passed / 3 skipped
files, 2896 tests passed.
- `@objectstack/cli`, `--project unit` `test/i18n-` +
`test/platform-page-i18n-parity`: 22 files / 292 passed. The integration
tier is declared to CI.
- `@objectstack/service-analytics`, full suite: 150 files / 3458 passed.
Its `typecheck` → 0.
- `@objectstack/platform-objects`, full suite: 59 files / 948 passed.
- `@objectstack/plugin-approvals`, `src/translations`: 3 files / 11
passed.
- `dispatch-gates.mjs --commands` over this diff (84 families) plus
`check:i18n`, `check:i18n-coverage` and `check:i18n-walk-parity`: 83 of
84 ran and exited 0. `check:dual-build-cjs-loads` first answered
PREREQUISITE NOT MET (exit 3) and exited 0 after the missing packages
were built. `--ran` reconciliation: 84 derived, 83 run, 0 NOT-MEASURED,
1 UNRUN.
- NOT MEASURED: `check:type-check-debt`. Reason: `--re-measure` runs tsc
over the whole workspace and was killed twice at the 570 s foreground
budget; it has no narrowing option. CI's `Lint & Repo Gates` owns it.
The type-check this diff owes was measured: spec and service-analytics
`typecheck` both 0.
- **Lint, a measured narrowing.** `eslint --no-inline-config --format
json` over the two changed TS files: 2 files, 0 errors, 0 warnings. Both
files are in the population, by `eslint --print-config`.
`eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move a verdict on any
untouched file. The full `pnpm lint` run belongs to CI.
## Acceptance notes
- The extractor's `pushViewEntries` docblock
(`packages/cli/src/utils/i18n-extract.ts`) names `translateView` as the
server-side reader of `_views.VIEW.label` / `.description`. After this
PR, `translateObject` is a second server-side reader, for the
object-nested channel. The docblock lags in a file outside this card's
surface. Noted, not filed.
- `_views.VIEW.emptyState.*` is still read client-side only, on both
channels. That is unchanged and is stated as design in the extractor's
docblock.
- This branch is based on `2f2fa11d7` and was not merged with `main`
before opening. `origin/main` is 21 commits ahead. None of those commits
touches `i18n-resolver.ts`, its tests, the REST meta read gate, the
extractor or the platform catalogs. Their `ui/view.zod.ts` change is
docblock-only, and their `rest-server.ts` change is outside the
translation path. PR CI runs on the merge ref.
## objectui follow-up (not in this PR)
objectui's `packages/app-shell/src/hooks/useServedViewItems.ts`
documents the `/meta/object`-embedded views as untranslated by the
server. Once this lands, `isServedView` should also answer true for a
view id that is a key of the served `/meta/object` document's
`listViews`, read from the pre-merge
`useMetadata().getItemsByType('object')`. Then `viewTabLabel`
(`ObjectView.tsx`) and the `AppHeader` crumb draw those labels as given
instead of a second `viewLabel` pass, in which the bundle would win an
edit back. The seat files that child after this lands.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6f57888 commit c27404f
3 files changed
Lines changed: 466 additions & 21 deletions
File tree
- .changeset
- packages/spec/src/system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 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 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 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 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
0 commit comments