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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Correct 17 false behavioral claims in skills/objectstack-i18n — sweep flight ⑧ by huangyiirene · Pull Request #13833 · objectstack-ai/objectstack · GitHub
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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Correct 17 false behavioral claims in skills/objectstack-i18n — sweep flight ⑧ by huangyiirene · Pull Request #13833 · objectstack-ai/objectstack · GitHub
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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Correct 17 false behavioral claims in skills/objectstack-i18n — sweep flight ⑧ by huangyiirene · Pull Request #13833 · objectstack-ai/objectstack · GitHub
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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Correct 17 false behavioral claims in skills/objectstack-i18n — sweep flight ⑧ by huangyiirene · Pull Request #13833 · objectstack-ai/objectstack · GitHub
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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Correct 17 false behavioral claims in skills/objectstack-i18n — sweep flight ⑧ by huangyiirene · Pull Request #13833 · objectstack-ai/objectstack · GitHub
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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Correct 17 false behavioral claims in skills/objectstack-i18n — sweep flight ⑧ by huangyiirene · Pull Request #13833 · objectstack-ai/objectstack · GitHub
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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Correct 17 false behavioral claims in skills/objectstack-i18n — sweep flight ⑧ by huangyiirene · Pull Request #13833 · objectstack-ai/objectstack · GitHub
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
94 changes: 43 additions & 51 deletions skills/objectstack-i18n/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,9 +47,9 @@ and integration with the I18nService.

1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one
`TranslationData` value. All translatable content for an object (label, fields,
options, views, sections, actions) is grouped under `objects.{object_name}`, with
global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`,
`metadataForms`) at the top level.
options, views, sections, tabs, actions) is grouped under `objects.{object_name}`,
with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`,
`flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level.

2. **Bundle registration**: per-locale files are assembled with
`defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle`
Expand DownExpand Up@@ -152,7 +152,7 @@ i18n/

The canonical authoring path: one `TranslationData` per locale, assembled with
`defineTranslationBundle` and registered on the stack. This mirrors the shipped
example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`):
`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`):

<!-- os:check -->
```typescript
Expand DownExpand Up@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under

| Sub-key | Holds |
|:--------|:------|
| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) |
| `label` / `pluralLabel` / `description` | Object-level text (every key optional) |
| `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field |
| `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` |
| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section / tab `label`, `description` |
| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` |
| `_sections.{section_name}` | Form section `label`, `description` |
| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) |

Top-level groups alongside `objects`: `apps` (label, description, navigation),
`messages`, `globalActions` (object-less actions), `dashboards`, `settings`,
`metadataForms`, `settingsCommon`.
`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`,
`settings`, `metadataForms`, `settingsCommon`.

> **Validation messages are not a translation group.** `validationMessages` was
> removed in spec 17.0.0 — nothing ever read it, so a translated rule
Expand DownExpand Up@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.

`os validate` / `os lint` / `os compile` check this direction and report it as
warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key
naming an object, field, view, action, param, section, app, nav item, dashboard
or widget that does not exist is listed alongside the names that do. A bundle
keyed to something since renamed still parses — the label just renders silently
in its source locale while every neighbouring label resolves.
naming an object, field, view, action, param, section, app, nav item, dashboard,
widget or flow screen that does not exist is listed alongside the names that do.
A bundle keyed to something since renamed still parses — the label just renders
silently in its source locale while every neighbouring one resolves.

---

Expand DownExpand Up@@ -342,9 +343,8 @@ export default defineTranslation({
Rules that differ from a file bundle:

- **`locale` is required.** A file bundle names its locales as map keys; an item
carries its own. An item whose locale cannot be resolved is skipped by the
runtime sync — a silent skip, which is why the field is mandatory rather
than inferred from the item name.
carries its own. The sync falls back to a BCP-47-looking item *name*, then skips
the item with an `[i18n] … — skipped` warning nobody watches.
- **One locale per item.** Author `zh-CN` and `ja-JP` as two items.
- Published items are loaded at boot and on every publish (no restart), and
layer **over** the file bundles — an authored value wins over a shipped one
Expand All@@ -357,11 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`

A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`,
`dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`,
`namespace`, and `_actions.confirmMessage`) was once documented for
Studio-authored translations. **No resolver ever read it**, so items authored
that way saved successfully and rendered nothing. It was removed —
those keys are now rejected at save time with a message naming the group to
use instead. Never author them, in files or at runtime.
`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring.
**No resolver ever read it**, and both doors now reject it — files included.

---

Expand DownExpand Up@@ -411,11 +408,11 @@ os i18n check --locales=zh-CN # scope to specific locales
os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
```

It compares registered bundles against source metadata and reports missing
object/field/option/view/action keys per locale. Missing keys in the default
locale are errors; `--strict` promotes non-default gaps to errors and
`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same
gate into linting.
It compares registered bundles against source metadata and reports missing keys
per locale for every surface the extractor walks — objects and their sub-keys,
global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in
the default locale are errors, `--strict` promotes the rest, `--show-keys` lists
them all; `os lint --i18n-strict` folds the same gate into lint.

### `os i18n extract --check` — freshness, not coverage

Expand All@@ -434,10 +431,8 @@ missing file and printing the regenerate command.
**Use both gates — they answer different questions.** `os i18n check` asks *are
the strings translated?* (coverage: human work). `extract --check` asks *are the
generated bundles still what the schema produces?* (freshness: machine output).
Renaming a label, adding an object, or removing a spec key leaves coverage at
100% while the bundles quietly go stale — which is exactly how the platform's
own bundles ended up carrying translations for keys the schema had already
deleted, plus fields with no entry in any locale.
Renaming a label or removing a spec key leaves coverage at 100% while bundles go
stale — how the platform's own ended up translating keys the schema had deleted.

It runs in the same **merge mode** as a normal extract, so it never asks for
re-translation: an up-to-date bundle re-extracts byte-identically. Requires
Expand All@@ -448,8 +443,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
The spec models coverage results for tooling: `TranslationCoverageResult`
(totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` —
`key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional
`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`,
`aiConfidence`). Full Zod shape:
`objectName`, optional `sourceHash` for stale detection, and AI-enrichment
fields (`aiSuggested`, `aiConfidence`). Full Zod shape:
`node_modules/@objectstack/spec/src/system/translation.zod.ts` —
`TranslationCoverageResultSchema`, `TranslationDiffItemSchema`.

Expand DownExpand Up@@ -498,15 +493,15 @@ registers when no i18n plugin is present):
- **`getTranslations(locale)`** — full snapshot for a locale
- **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple
plugins can each contribute their own `objects.*` slice
- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**
- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`**

The in-memory fallback additionally resolves locale codes
(exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`).

The contract also declares optional methods — `getCoverage`,
The contract also declares optional methods — `getFieldLabels`, `getCoverage`,
`suggestTranslations` — that **no shipped implementation provides**. Treat them
as extension points for a custom workbench or TMS adapter. (`getAppBundle` /
`loadAppBundle` were removed along with the `o.*` shape they returned.)
`loadAppBundle` went with the `o.*` shape they returned.)

### Plugin Setup

Expand DownExpand Up@@ -545,10 +540,11 @@ Scaffold ready-to-edit translation files from your stack config:
os i18n extract --locales=zh-CN --out=./src/translations
```

This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON) — the
default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex
over object/app names or key paths), `--dry-run`, `--json`.
This writes `<locale>.objects.generated.ts` TypeScript modules (not JSON), plus a
`<locale>.metadata-forms.generated.ts` companion unless `--no-metadata-forms` —
the default locale is filled from schema labels, other locales follow `--fill`
(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`,
`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, …

### 2. Translate

Expand All@@ -572,18 +568,16 @@ Commit the translation files, import them into your bundle, and register it via

## CRM I18n Blueprint

Reference implementation shape:

- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`)
- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts`
`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` +
`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`).

Use this structure for metadata apps:

| Layer | CRM Pattern |
|:--|:--|
| Stack config | `i18n` with an explicit locale list; per-locale source files by convention |
| Translation assembly | One `defineTranslationBundle` call that imports per-locale files |
| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages |
| Stack config | `i18n` with an explicit locale list; the source layout is a convention |
| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files |
| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages |
| Naming integrity | Translation object/field keys exactly match metadata machine names |

For new locales, copy one locale file as a baseline, then run `os i18n check`
Expand All@@ -595,10 +589,8 @@ before release.

### ❌ The Retired `o.*` Shape

Everything reads `objects.*`. The `o.*` dialect was removed — it is
not a "Studio format", not a secondary format, just gone. Files registered in
that shape resolve to nothing; runtime items in that shape are rejected at
save time.
Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio
format", not a secondary format, just gone. Both doors reject it, files included.

```typescript
// WRONG — in a file bundle AND in a `translation` item
Expand DownExpand Up@@ -647,7 +639,7 @@ options: { in_progress: '进行中' }

### ❌ Ignoring Coverage Reports

Stale translations can cause confusion. Always run `os i18n check` before releases.
Run `os i18n check` before releases; `extract --check` is what sees staleness.

---

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-i18n/evals/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,11 @@ When implemented, evals will follow this structure:
```
evals/
├── bundle-shape/
│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format
│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected
│ ├── test-snake-case-keys.md # object/field keys match metadata machine names
│ └── test-option-machine-values.md # lowercase option values, not display labels
├── interpolation/
│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental
│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine
├── coverage-workflow/
│ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output
│ └── test-check-command.md # os i18n check --strict/--threshold CI gate
Expand Down
Loading