Skip to content

Commit 8dcd6a4

Browse files
committed
fix(spec): derive the migration TODO's group list from the schema, not a literal
The semantic migration entry for `translation-per-app-settings-platform-only` enumerated nine of the ten groups the per-app face declares and omitted `settingsCommon` — the nearest legitimate neighbour for an author who has just had `settings` refused. `os migrate meta --from 17` prints this string verbatim to the operator (`packages/cli/src/commands/migrate/meta.ts` renders `surface → replacement`), so the omission reached a user-facing surface. Correcting the literal would leave the construct that produced it. A hand-maintained copy of a schema's key set already drifted here once, through a full review of the surrounding change, so the copy is deleted rather than pinned: `replacement` is now a getter that reads `Object.keys(TranslationDataSchema.shape)`. There is one spelling of the set, and a group added to the per-app face reaches this message the day it is declared, with nothing to remember. Ordered remedy — delete the construct that permits the error before reaching for a check that only reddens it. `Object.keys` on a zod object shape returns the declaration order of the literal it was built from, which is the order the sentence promises; the separator and the shape of the sentence are unchanged, so the rendered paragraph reads as it did. A getter rather than an eager template because importing the registry must not force the lazy translation schema at module load. The registry is generated by concatenating entry literals, and the generator treats a file's imports as scaffolding — so `registry.ts` carries its own hand-written value import outside the generated regions, with a comment saying why. Verified: it survives `gen:migration-registry`, and `system/translation.zod.ts`'s own 26-module closure reaches nothing under `migrations/`, so the edge adds no cycle. The changeset gains one sentence naming `settingsCommon` as unaffected. The declared semver level is untouched — both packages stay `minor`; two prose corrections and one derivation move no published signature. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent b1e7984 commit 8dcd6a4

3 files changed

Lines changed: 61 additions & 22 deletions

File tree

‎.changeset/15178-translation-bundle-split-settings-platform-only.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ included.
2828
is not application-authorable at all — `settings` is keyed by
2929
`SettingsManifest.namespace` and only platform code declares a manifest, so the
3030
only namespaces a per-app entry could ever address were the platform's own.
31+
`settingsCommon` is **not** affected — the Settings UI shell strings (the source
32+
badges, under `settingsCommon.sourceLabels`) stay on the per-app face; only the
33+
per-namespace manifest copy under `settings` leaves.
3134
Run `os migrate meta --from 17` to list the mechanical edits for existing
3235
sources; apply them by hand.
3336

‎packages/spec/src/migrations/entries/semantic/18.translation-per-app-settings-platform-only.ts‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
import { TranslationDataSchema } from '../../../system/translation.zod.js';
34
import type { SemanticMigration } from '../../types.js';
45

56
// The judgment half of `translation-per-app-settings-removed`. The D2
@@ -10,17 +11,31 @@ import type { SemanticMigration } from '../../types.js';
1011
export const entry: SemanticMigration = {
1112
id: 'translation-per-app-settings-platform-only',
1213
surface: 'stack.translations[].<locale>.settings — the per-app bundle’s settings group',
13-
replacement:
14-
'Delete the group from the per-app bundle. There is no per-app replacement key: settings copy '
15-
+ 'is not application-authorable at all. `settings` is keyed by `SettingsManifest.namespace`, '
16-
+ 'and only platform code declares a manifest '
17-
+ '(`packages/services/service-settings/src/manifests/*.manifest.ts`), so the only namespaces a '
18-
+ 'per-app entry could ever address were the platform’s own. Platform settings copy is '
19-
+ 'translated in the PLATFORM bundle — `@objectstack/service-settings`’s '
20-
+ '`settingsBuiltinTranslations`, typed `PlatformTranslationData` — which is where a correction '
21-
+ 'to a platform string belongs. An application’s own copy goes in the groups the per-app bundle '
22-
+ 'still declares: `objects`, `apps`, `pages`, `dashboards`, `datasets`, `flows`, '
23-
+ '`globalActions`, `metadataForms`, `messages`.',
14+
// The group names are DERIVED from `TranslationDataSchema.shape`, never typed
15+
// out beside it. A hand-maintained copy of a schema's key set is the construct
16+
// that drifted to nine-of-ten in this very message, so the copy is deleted
17+
// rather than pinned: there is one spelling of the set, and a group added to
18+
// the per-app face reaches this sentence the day it is declared.
19+
// `Object.keys` on a zod object shape yields the declaration order of the
20+
// literal it was built from — the order this sentence promises the operator.
21+
// A getter, not an eager template: importing the registry must not force the
22+
// lazy translation schema at module load.
23+
get replacement(): string {
24+
const groups = Object.keys(TranslationDataSchema.shape);
25+
return 'Delete the group from the per-app bundle. There is no per-app replacement key: settings copy '
26+
+ 'is not application-authorable at all. `settings` is keyed by `SettingsManifest.namespace`, '
27+
+ 'and only platform code declares a manifest '
28+
+ '(`packages/services/service-settings/src/manifests/*.manifest.ts`), so the only namespaces a '
29+
+ 'per-app entry could ever address were the platform’s own. Platform settings copy is '
30+
+ 'translated in the PLATFORM bundle — `@objectstack/service-settings`’s '
31+
+ '`settingsBuiltinTranslations`, typed `PlatformTranslationData` — which is where a correction '
32+
+ 'to a platform string belongs. An application’s own copy goes in the '
33+
+ `${groups.length} groups the per-app bundle still declares, in the order it declares them: `
34+
+ groups.map((g) => `\`${g}\``).join(', ')
35+
+ '. Note `settingsCommon` among them: it IS on this face, so the Settings UI shell strings an '
36+
+ 'application may translate (the source badges, under `settingsCommon.sourceLabels`) are NOT '
37+
+ 'what is being removed here — only the per-namespace manifest copy under `settings` is.';
38+
},
2439
reason:
2540
'Not losslessly convertible, and NOT because the content was inert — but not because it '
2641
+ 'overrode anything either. Measured on this tree before the split: '

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
* hand-written and still merges as text.
3939
*/
4040

41+
// A VALUE import, and the only one here: an entry literal below derives its
42+
// group enumeration from this schema's own keys rather than restating them
43+
// (`translation-per-app-settings-platform-only`). Entry files carry their own
44+
// copy of this import, but the generator treats a file's imports as scaffolding
45+
// and concatenates only the literal — so an entry that references a value needs
46+
// that value in scope HERE, hand-written, outside the generated regions.
47+
import { TranslationDataSchema } from '../system/translation.zod.js';
4148
import type { MigrationStep } from './types.js';
4249

4350
/**
@@ -12238,17 +12245,31 @@ const step18: MigrationStep = {
1223812245
{
1223912246
id: 'translation-per-app-settings-platform-only',
1224012247
surface: 'stack.translations[].<locale>.settings — the per-app bundle’s settings group',
12241-
replacement:
12242-
'Delete the group from the per-app bundle. There is no per-app replacement key: settings copy '
12243-
+ 'is not application-authorable at all. `settings` is keyed by `SettingsManifest.namespace`, '
12244-
+ 'and only platform code declares a manifest '
12245-
+ '(`packages/services/service-settings/src/manifests/*.manifest.ts`), so the only namespaces a '
12246-
+ 'per-app entry could ever address were the platform’s own. Platform settings copy is '
12247-
+ 'translated in the PLATFORM bundle — `@objectstack/service-settings`’s '
12248-
+ '`settingsBuiltinTranslations`, typed `PlatformTranslationData` — which is where a correction '
12249-
+ 'to a platform string belongs. An application’s own copy goes in the groups the per-app bundle '
12250-
+ 'still declares: `objects`, `apps`, `pages`, `dashboards`, `datasets`, `flows`, '
12251-
+ '`globalActions`, `metadataForms`, `messages`.',
12248+
// The group names are DERIVED from `TranslationDataSchema.shape`, never typed
12249+
// out beside it. A hand-maintained copy of a schema's key set is the construct
12250+
// that drifted to nine-of-ten in this very message, so the copy is deleted
12251+
// rather than pinned: there is one spelling of the set, and a group added to
12252+
// the per-app face reaches this sentence the day it is declared.
12253+
// `Object.keys` on a zod object shape yields the declaration order of the
12254+
// literal it was built from — the order this sentence promises the operator.
12255+
// A getter, not an eager template: importing the registry must not force the
12256+
// lazy translation schema at module load.
12257+
get replacement(): string {
12258+
const groups = Object.keys(TranslationDataSchema.shape);
12259+
return 'Delete the group from the per-app bundle. There is no per-app replacement key: settings copy '
12260+
+ 'is not application-authorable at all. `settings` is keyed by `SettingsManifest.namespace`, '
12261+
+ 'and only platform code declares a manifest '
12262+
+ '(`packages/services/service-settings/src/manifests/*.manifest.ts`), so the only namespaces a '
12263+
+ 'per-app entry could ever address were the platform’s own. Platform settings copy is '
12264+
+ 'translated in the PLATFORM bundle — `@objectstack/service-settings`’s '
12265+
+ '`settingsBuiltinTranslations`, typed `PlatformTranslationData` — which is where a correction '
12266+
+ 'to a platform string belongs. An application’s own copy goes in the '
12267+
+ `${groups.length} groups the per-app bundle still declares, in the order it declares them: `
12268+
+ groups.map((g) => `\`${g}\``).join(', ')
12269+
+ '. Note `settingsCommon` among them: it IS on this face, so the Settings UI shell strings an '
12270+
+ 'application may translate (the source badges, under `settingsCommon.sourceLabels`) are NOT '
12271+
+ 'what is being removed here — only the per-namespace manifest copy under `settings` is.';
12272+
},
1225212273
reason:
1225312274
'Not losslessly convertible, and NOT because the content was inert — but not because it '
1225412275
+ 'overrode anything either. Measured on this tree before the split: '

0 commit comments

Comments
 (0)