Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .changeset/i18n-provenance-served-in-eight-sets.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
---
"@objectstack/plugin-approvals": patch
"@objectstack/plugin-audit": patch
"@objectstack/plugin-security": patch
"@objectstack/plugin-sharing": patch
"@objectstack/plugin-webhooks": patch
"@objectstack/service-messaging": patch
"@objectstack/service-realtime": patch
"@objectstack/service-storage": patch
---

fix(i18n): read the provenance companion at serving time, not only record it (#12642)

Maintainer ruling #12069 Option A (#11671) landed translation provenance as
**two** halves: `os i18n extract --source-hashes` RECORDS which source revision
a generated leaf is still a byte copy of, and `withSourceFallback` READS those
records at serving time and substitutes the current source for a leaf whose
source has moved underneath it. The recording half was then rolled out to every
bundle set. The reading half was not — measured on `main`: provenance
**recorded in 9 of 9** bundle sets and **read at serving time in 1**.

The other eight assembled their `TranslationBundle` straight from the raw
generated modules and never consulted the companion sitting beside them, so
they recorded the drift and went on serving the superseded draft. Nothing said
so: `check:i18n` compares key sets and they still matched, `check:i18n-coverage`
counts a present leaf as translated, and `check:i18n-stale-fill`'s cross-locale
rule needs a SECOND locale holding the same stale bytes before it can testify.
The measured case had one locale and no second witness.

All eight are wired here, in the shape `@objectstack/platform-objects`'s own
`metadata-translations/index.ts` uses — the committed
`<locale>.source-hashes.generated.ts` passed as the fourth argument, the third
left `undefined` because these sets have no hand-authored sections. Provenance
is now recorded in 9 of 9 sets and served in 9 of 9.

`@objectstack/plugin-webhooks` was the last of them and is the only one whose
manifest changed: `withSourceFallback` lives in `@objectstack/platform-objects`,
which that package did not declare. It was **already in that package's install
closure** through `@objectstack/service-messaging`, so the edge declares a
resolution that already resolved rather than adding a package to the graph —
and relying on it undeclared would have been a phantom dependency under this
repo's strict package manager.

`check:i18n-stale-fill` gains a second verdict, **UNSERVED PROVENANCE**, so this
cannot silently come apart again: a bundle set that commits a companion and
does not consult it at serving time now fails the build, including a tenth set
that lands tomorrow.

**Graded `patch`, and the grade is the interesting part.** No API changes, no
new exported surface, and no key set moves — substitution was chosen over
deletion precisely so key-set claims stay put (ruling #8765 Option B). What
changes is which STRING a stale leaf serves. On this tree that is **zero
leaves**: a record is only ever written for a leaf that IS a byte copy of the
current source, so the companions arrive 0-stale by construction. The change is
in what happens the next time a source string moves — the reader sees the
English source rather than a superseded draft of it, which is the same
degradation an untranslated key already produces and not a new state.
43 changes: 38 additions & 5 deletions packages/plugins/plugin-approvals/src/translations/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,48 @@
* `scripts/i18n-extract.config.ts`.
*/

import type { TranslationBundle } from '@objectstack/spec/system';
import type { TranslationBundle, TranslationData } from '@objectstack/spec/system';
import { withSourceFallback } from '@objectstack/platform-objects/apps';
import { enObjects } from './en.objects.generated.js';
import { zhCNObjects } from './zh-CN.objects.generated.js';
import { jaJPObjects } from './ja-JP.objects.generated.js';
import { esESObjects } from './es-ES.objects.generated.js';
import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js';
import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js';
import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js';

/**
* ## The provenance companions are READ here, not merely recorded
*
* `os i18n extract --source-hashes` writes `<locale>.source-hashes.generated.ts`
* beside these bundles (maintainer ruling #12069 Option A, #11671). A record
* says: "this locale's leaf at that path is still a byte copy of THAT source
* revision". Recording alone changes nothing a user sees — the substitution is
* what {@link withSourceFallback} does, and until it was wired here this set
* recorded the drift and went on serving the superseded draft.
*
* That gap was invisible by construction: a leaf revised in ONE locale is
* reported by `findStaleFills`, and every gate stays green — `check:i18n`
* compares key sets, `check:i18n-coverage` counts a stale leaf as translated,
* and `check:i18n-stale-fill` needs two locales holding the same stale bytes
* before it can testify. So the only reader-visible consequence was the wrong
* string on the page.
*
* `recorded` (3rd argument) stays `undefined` on purpose: it judges the
* HAND-AUTHORED sections (`apps` / `dashboards` / `pages`), which this set does
* not have — its bundles are entirely generated. The companion goes in the 4th
* slot, which judges the generated ones. This is the shape
* `@objectstack/platform-objects`'s own `metadata-translations/index.ts` uses.
*
* ⛔ Do not drop the 4th argument to quiet a staleness report. Serving the
* superseded draft is the bug; `check:i18n-stale-fill`'s UNSERVED PROVENANCE
* verdict fails the build if a committed companion stops being consulted here.
*/
const enSource: TranslationData = { objects: enObjects };

export const ApprovalsTranslations: TranslationBundle = {
en: { objects: enObjects },
'zh-CN': { objects: zhCNObjects },
'ja-JP': { objects: jaJPObjects },
'es-ES': { objects: esESObjects },
en: enSource,
'zh-CN': withSourceFallback({ objects: zhCNObjects }, enSource, undefined, zhCNGeneratedSourceHashes),
'ja-JP': withSourceFallback({ objects: jaJPObjects }, enSource, undefined, jaJPGeneratedSourceHashes),
'es-ES': withSourceFallback({ objects: esESObjects }, enSource, undefined, esESGeneratedSourceHashes),
};
11 changes: 10 additions & 1 deletion packages/plugins/plugin-approvals/tsconfig.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,16 @@
// (`pnpm check:type-source-resolution` — same fix `packages/rest`
// records for `@objectstack/metadata-protocol`).
"paths": {
"@objectstack/metadata-core": ["../../metadata-core/src/index.ts"]
"@objectstack/metadata-core": ["../../metadata-core/src/index.ts"],
// [#12642] Resolve the i18n provenance seam to SOURCE. This package's
// translation barrel passes its committed
// `<locale>.source-hashes.generated.ts` companions through
// `withSourceFallback`, whose home is `@objectstack/platform-objects/apps`
// — a NEW type import here, so without this rule the typecheck would be a
// verdict about that package's `dist/` build state
// (`pnpm check:type-source-resolution`). Anchored on the SUBPATH: the
// bare key would match by prefix and resolve `/apps` through a file.
"@objectstack/platform-objects/apps": ["../../platform-objects/src/apps/index.ts"]
}
},
"include": ["src/**/*"],
Expand Down
43 changes: 38 additions & 5 deletions packages/plugins/plugin-audit/src/translations/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,16 +10,49 @@
* `scripts/i18n-extract.config.ts`.
*/

import type { TranslationBundle } from '@objectstack/spec/system';
import type { TranslationBundle, TranslationData } from '@objectstack/spec/system';
import { withSourceFallback } from '@objectstack/platform-objects/apps';
import { enObjects } from './en.objects.generated.js';
import { zhCNObjects } from './zh-CN.objects.generated.js';
import { jaJPObjects } from './ja-JP.objects.generated.js';
import { esESObjects } from './es-ES.objects.generated.js';
import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js';
import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js';
import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js';
import { enMessages, zhCNMessages, jaJPMessages, esESMessages } from './messages.js';

/**
* ## The provenance companions are READ here, not merely recorded
*
* `os i18n extract --source-hashes` writes `<locale>.source-hashes.generated.ts`
* beside these bundles (maintainer ruling #12069 Option A, #11671). A record
* says: "this locale's leaf at that path is still a byte copy of THAT source
* revision". Recording alone changes nothing a user sees — the substitution is
* what {@link withSourceFallback} does, and until it was wired here this set
* recorded the drift and went on serving the superseded draft.
*
* That gap was invisible by construction: a leaf revised in ONE locale is
* reported by `findStaleFills`, and every gate stays green — `check:i18n`
* compares key sets, `check:i18n-coverage` counts a stale leaf as translated,
* and `check:i18n-stale-fill` needs two locales holding the same stale bytes
* before it can testify. So the only reader-visible consequence was the wrong
* string on the page.
*
* `recorded` (3rd argument) stays `undefined` on purpose: it judges the
* HAND-AUTHORED sections (`apps` / `dashboards` / `pages`), which this set does
* not have — its bundles are entirely generated. The companion goes in the 4th
* slot, which judges the generated ones. This is the shape
* `@objectstack/platform-objects`'s own `metadata-translations/index.ts` uses.
*
* ⛔ Do not drop the 4th argument to quiet a staleness report. Serving the
* superseded draft is the bug; `check:i18n-stale-fill`'s UNSERVED PROVENANCE
* verdict fails the build if a committed companion stops being consulted here.
*/
const enSource: TranslationData = { objects: enObjects, messages: enMessages };

export const AuditTranslations: TranslationBundle = {
en: { objects: enObjects, messages: enMessages },
'zh-CN': { objects: zhCNObjects, messages: zhCNMessages },
'ja-JP': { objects: jaJPObjects, messages: jaJPMessages },
'es-ES': { objects: esESObjects, messages: esESMessages },
en: enSource,
'zh-CN': withSourceFallback({ objects: zhCNObjects, messages: zhCNMessages }, enSource, undefined, zhCNGeneratedSourceHashes),
'ja-JP': withSourceFallback({ objects: jaJPObjects, messages: jaJPMessages }, enSource, undefined, jaJPGeneratedSourceHashes),
'es-ES': withSourceFallback({ objects: esESObjects, messages: esESMessages }, enSource, undefined, esESGeneratedSourceHashes),
};
11 changes: 10 additions & 1 deletion packages/plugins/plugin-audit/tsconfig.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,16 @@
// (`pnpm check:type-source-resolution` — same fix `packages/rest`
// records for `@objectstack/metadata-protocol`).
"paths": {
"@objectstack/metadata-core": ["../../metadata-core/src/index.ts"]
"@objectstack/metadata-core": ["../../metadata-core/src/index.ts"],
// [#12642] Resolve the i18n provenance seam to SOURCE. This package's
// translation barrel passes its committed
// `<locale>.source-hashes.generated.ts` companions through
// `withSourceFallback`, whose home is `@objectstack/platform-objects/apps`
// — a NEW type import here, so without this rule the typecheck would be a
// verdict about that package's `dist/` build state
// (`pnpm check:type-source-resolution`). Anchored on the SUBPATH: the
// bare key would match by prefix and resolve `/apps` through a file.
"@objectstack/platform-objects/apps": ["../../platform-objects/src/apps/index.ts"]
}
},
"include": [
Expand Down
9 changes: 9 additions & 0 deletions packages/plugins/plugin-audit/vitest.config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,15 @@ export default defineConfig({
find: /^@objectstack\/platform-objects\/audit$/,
replacement: path.resolve(__dirname, '../../platform-objects/src/audit/index.ts'),
},
// [#12642] The i18n provenance seam. This package's translation barrel
// passes its committed `<locale>.source-hashes.generated.ts` companions
// through `withSourceFallback`, whose home is this subpath — so without
// the alias the suite's verdict would be about `platform-objects/dist`
// build state rather than the checkout (`pnpm check:test-source-alias`).
{
find: /^@objectstack\/platform-objects\/apps$/,
replacement: path.resolve(__dirname, '../../platform-objects/src/apps/index.ts'),
},
// Covers `data` / `system` / `kernel` / `api` / `contracts` / `ui` /
// `shared` and, [ADR-0105 D1], `security` reached transitively via
// `@objectstack/types` (tenancy posture).
Expand Down
43 changes: 38 additions & 5 deletions packages/plugins/plugin-security/src/translations/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,48 @@
* `scripts/i18n-extract.config.ts`.
*/

import type { TranslationBundle } from '@objectstack/spec/system';
import type { TranslationBundle, TranslationData } from '@objectstack/spec/system';
import { withSourceFallback } from '@objectstack/platform-objects/apps';
import { enObjects } from './en.objects.generated.js';
import { zhCNObjects } from './zh-CN.objects.generated.js';
import { jaJPObjects } from './ja-JP.objects.generated.js';
import { esESObjects } from './es-ES.objects.generated.js';
import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js';
import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js';
import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js';

/**
* ## The provenance companions are READ here, not merely recorded
*
* `os i18n extract --source-hashes` writes `<locale>.source-hashes.generated.ts`
* beside these bundles (maintainer ruling #12069 Option A, #11671). A record
* says: "this locale's leaf at that path is still a byte copy of THAT source
* revision". Recording alone changes nothing a user sees — the substitution is
* what {@link withSourceFallback} does, and until it was wired here this set
* recorded the drift and went on serving the superseded draft.
*
* That gap was invisible by construction: a leaf revised in ONE locale is
* reported by `findStaleFills`, and every gate stays green — `check:i18n`
* compares key sets, `check:i18n-coverage` counts a stale leaf as translated,
* and `check:i18n-stale-fill` needs two locales holding the same stale bytes
* before it can testify. So the only reader-visible consequence was the wrong
* string on the page.
*
* `recorded` (3rd argument) stays `undefined` on purpose: it judges the
* HAND-AUTHORED sections (`apps` / `dashboards` / `pages`), which this set does
* not have — its bundles are entirely generated. The companion goes in the 4th
* slot, which judges the generated ones. This is the shape
* `@objectstack/platform-objects`'s own `metadata-translations/index.ts` uses.
*
* ⛔ Do not drop the 4th argument to quiet a staleness report. Serving the
* superseded draft is the bug; `check:i18n-stale-fill`'s UNSERVED PROVENANCE
* verdict fails the build if a committed companion stops being consulted here.
*/
const enSource: TranslationData = { objects: enObjects };

export const SecurityTranslations: TranslationBundle = {
en: { objects: enObjects },
'zh-CN': { objects: zhCNObjects },
'ja-JP': { objects: jaJPObjects },
'es-ES': { objects: esESObjects },
en: enSource,
'zh-CN': withSourceFallback({ objects: zhCNObjects }, enSource, undefined, zhCNGeneratedSourceHashes),
'ja-JP': withSourceFallback({ objects: jaJPObjects }, enSource, undefined, jaJPGeneratedSourceHashes),
'es-ES': withSourceFallback({ objects: esESObjects }, enSource, undefined, esESGeneratedSourceHashes),
};
43 changes: 38 additions & 5 deletions packages/plugins/plugin-sharing/src/translations/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,48 @@
* `scripts/i18n-extract.config.ts`.
*/

import type { TranslationBundle } from '@objectstack/spec/system';
import type { TranslationBundle, TranslationData } from '@objectstack/spec/system';
import { withSourceFallback } from '@objectstack/platform-objects/apps';
import { enObjects } from './en.objects.generated.js';
import { zhCNObjects } from './zh-CN.objects.generated.js';
import { jaJPObjects } from './ja-JP.objects.generated.js';
import { esESObjects } from './es-ES.objects.generated.js';
import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js';
import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js';
import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js';

/**
* ## The provenance companions are READ here, not merely recorded
*
* `os i18n extract --source-hashes` writes `<locale>.source-hashes.generated.ts`
* beside these bundles (maintainer ruling #12069 Option A, #11671). A record
* says: "this locale's leaf at that path is still a byte copy of THAT source
* revision". Recording alone changes nothing a user sees — the substitution is
* what {@link withSourceFallback} does, and until it was wired here this set
* recorded the drift and went on serving the superseded draft.
*
* That gap was invisible by construction: a leaf revised in ONE locale is
* reported by `findStaleFills`, and every gate stays green — `check:i18n`
* compares key sets, `check:i18n-coverage` counts a stale leaf as translated,
* and `check:i18n-stale-fill` needs two locales holding the same stale bytes
* before it can testify. So the only reader-visible consequence was the wrong
* string on the page.
*
* `recorded` (3rd argument) stays `undefined` on purpose: it judges the
* HAND-AUTHORED sections (`apps` / `dashboards` / `pages`), which this set does
* not have — its bundles are entirely generated. The companion goes in the 4th
* slot, which judges the generated ones. This is the shape
* `@objectstack/platform-objects`'s own `metadata-translations/index.ts` uses.
*
* ⛔ Do not drop the 4th argument to quiet a staleness report. Serving the
* superseded draft is the bug; `check:i18n-stale-fill`'s UNSERVED PROVENANCE
* verdict fails the build if a committed companion stops being consulted here.
*/
const enSource: TranslationData = { objects: enObjects };

export const SharingTranslations: TranslationBundle = {
en: { objects: enObjects },
'zh-CN': { objects: zhCNObjects },
'ja-JP': { objects: jaJPObjects },
'es-ES': { objects: esESObjects },
en: enSource,
'zh-CN': withSourceFallback({ objects: zhCNObjects }, enSource, undefined, zhCNGeneratedSourceHashes),
'ja-JP': withSourceFallback({ objects: jaJPObjects }, enSource, undefined, jaJPGeneratedSourceHashes),
'es-ES': withSourceFallback({ objects: esESObjects }, enSource, undefined, esESGeneratedSourceHashes),
};
Loading
Loading