Skip to content

Commit a36db28

Browse files
qq9340100claude
andauthored
feat(spec,cli): i18n 标签契约 —— 内联 locale map 授权化 + filter-only tab 的翻译槽 (#6512)
* feat(spec): i18n label contract — inline locale maps, `_tabs` translations Fixes #5728 Fixes #5377 `I18nLabelSchema` becomes a union of the plain default-language string and an inline BCP-47 locale map, and `ObjectTranslationDataSchema` gains `_tabs` with a resolver that reads it. See the PR body for the per-issue rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY * chore: changeset for the i18n label contract sweep Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY * chore(spec): regenerate artifacts on the merge; pin InlineLocaleMap under ADR-0122 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY * chore(spec): regenerate skill references and the react-blocks contract The union widening changes what the generated skill artifacts say about every `I18nLabelSchema` prop (`string` -> `string | object`), and `i18n.zod.ts` gains an explicit MODULE doc so its skill-index headline is a decision rather than a side effect of which declaration sorts first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY * chore(spec): regenerate generated surfaces from the merged tree os-regen four-step after merging origin/main: docs references, api-surface, authorable-surface (base + system). check:generated 10/10, check:spec-parsed-alias 756 OK, check:i18n-coverage none new. * chore(spec): regenerate generated surfaces from the post-sweep merged tree Second regen round after #6526 merged: docs references, api-surface, authorable-surface, strictness-ledger counts. check:generated 10/10, check:spec-parsed-alias 749 OK, pin test 3/3. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 54299ca commit a36db28

41 files changed

Lines changed: 1265 additions & 401 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
i18n label contract: inline locale maps are authorized, and filter-preset tabs get a translation slot
7+
8+
**`I18nLabelSchema` accepts two forms, not one (#5728).** It declared a plain
9+
`z.string()` while three published platform pages authored 31 inline
10+
`{ en, 'zh-CN', 'ja-JP', 'es-ES' }` maps that objectui resolves through
11+
`pickLocalized` — so the authoritative document was the wrong one, and the
12+
#5068 component-props gate reported 42 findings against the platform's own
13+
pages. The schema is now a union of the default-language string and an inline
14+
locale map. `ElementTextPropsSchema.content` was declared a bare `z.string()`
15+
and therefore out of that union's reach; it moves onto `I18nLabelSchema` in the
16+
same change, which is the other 8 of the 42. The gate now reports **0**.
17+
18+
This does not reverse #4667 / #5055. What those retired was the *key-reference*
19+
dialect (`{ key, defaultValue }`) — a shape with **no resolver**, whose label
20+
reached the screen as a raw key or not at all. What is authorized here is the
21+
inline locale map, which has a live resolver and which the CLI's `i18n-extract`
22+
already understands. Same "declared = enforced" principle, applied in both
23+
directions: the map's keys are constrained to BCP-47 tags (plus `default`), so
24+
`{ key, defaultValue }` stays a parse error rather than becoming "a locale map
25+
whose locales are named `key` and `defaultValue`".
26+
27+
Zero breaking: every previously-valid label is still valid. The
28+
translation-bundle channel remains the direction that scales and is unchanged.
29+
30+
**Filter-preset tab labels are translatable (#5377).** `ObjectTranslationData`
31+
gains `_tabs`, addressed by `ViewTabSchema.name`, and `resolveTabLabel` reads
32+
it — explicit `_tabs` translation, then the referenced view's `_views.*.label`
33+
for a tab that carries `view` (the path that already worked, preserved), then
34+
the authored literal. A tab carrying only a `filter` referenced nothing to
35+
inherit from and had no key of its own, so its label rendered in the source
36+
language above a fully localized grid with no authoring workaround. `os i18n
37+
extract` scaffolds the new keys, so the slot, the resolver and the extractor
38+
land together.
39+
40+
`I18nLabelSchema`'s description no longer claims "i18n keys are auto-generated
41+
by the framework" — none are. `AriaPropsSchema.ariaLabel` now states that no
42+
translation-bundle slot addresses it.

content/docs/references/api/metadata.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const result = AppDefinitionResponseSchema.parse(data);
5353
| **success** | `boolean` || Operation success status |
5454
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false |
5555
| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
56-
| **data** | `{ name: string; label: string; description?: string; icon?: string; … }` || Full App Configuration |
56+
| **data** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; icon?: string; … }` || Full App Configuration |
5757

5858

5959
---

content/docs/references/api/protocol.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -909,11 +909,11 @@ Get package response
909909
| Property | Type | Required | Description |
910910
| :--- | :--- | :--- | :--- |
911911
| **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. |
912-
| **label** | `string` | optional | Human-readable label shown in metadata lists. |
912+
| **label** | `string \| Record<string, string>` | optional | Human-readable label shown in metadata lists. |
913913
| **object** | `string` | optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. |
914-
| **list** | `{ name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }; … }` | optional | |
914+
| **list** | `{ name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }; … }` | optional | |
915915
| **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | |
916-
| **listViews** | `Record<string, { name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) |
916+
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) |
917917
| **formViews** | `Record<string, { type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views |
918918
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. |
919919
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

content/docs/references/data/object.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,13 @@ const result = ApiMethod.parse(data);
136136
| **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
137137
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
138138
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
139-
| **listViews** | `Record<string, { name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
139+
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record<string, any> }; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
140140
| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. |
141141
| **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]; … }` | optional | Enabled system features modules |
142142
| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). |
143143
| **externalSharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | [ADR-0090 D11] OWD for external (portal/partner) principals. Defaults to private; must be &lt;= sharingModel in openness. |
144144
| **publicSharing** | `{ enabled?: boolean; allowedAudiences?: Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]; allowedPermissions?: Enum<'view' \| 'comment' \| 'edit'>[]; maxExpiryDays?: integer; … }` | optional | Public share-link policy (Notion/Figma-style link sharing) |
145-
| **actions** | `{ name: string; label: string; objectName?: string; icon?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) |
145+
| **actions** | `{ name: string; label: string \| Record<string, string>; objectName?: string; icon?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) |
146146
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this object. |
147147
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
148148
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |

content/docs/references/index.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1582 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1583 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with
3232
| [Shared Protocol](/docs/references/shared) | 8 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3434
| [System Protocol](/docs/references/system) | 37 | 292 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
35-
| [UI Protocol](/docs/references/ui) | 16 | 146 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **199** | **1582** | 14 protocol modules |
35+
| [UI Protocol](/docs/references/ui) | 16 | 147 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36+
| **Total** | **199** | **1583** | 14 protocol modules |
3737

3838
---
3939

@@ -365,7 +365,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
365365

366366
## UI Protocol
367367

368-
**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 146 schemas**
368+
**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 147 schemas**
369369

370370
Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer.
371371

@@ -379,7 +379,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay
379379
| [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ElementButtonProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementTextInputProps`, `ElementTextProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordPathProps`, `RecordRelatedListProps` |
380380
| [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` |
381381
| [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetMeasure`, `DerivedMeasureOp` |
382-
| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel` |
382+
| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel`, `InlineLocaleMap` |
383383
| [`notification.zod.ts`](/docs/references/ui/notification) | `NotificationPosition`, `NotificationSeverity`, `NotificationType` |
384384
| [`page.zod.ts`](/docs/references/ui/page) | `ElementDataSource`, `InterfacePageConfig`, `Page`, `PageComponent`, `PageComponentType`, `PageRegion`, `PageType`, `PageVariable` |
385385
| [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` |

content/docs/references/kernel/manifest.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const result = ManifestSchema.parse(data);
4444
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
4545
| **capabilities** | `{ implements?: { protocol: object; conformance?: Enum<'full' \| 'partial' \| 'experimental' \| 'deprecated'>; implementedFeatures?: string[]; features?: { name: string; enabled?: boolean; description?: string; sinceVersion?: string; … }[]; … }[]; provides?: { id: string; name: string; description?: string; version: object; … }[]; requires?: { pluginId: string; version: string; optional?: boolean; reason?: string; … }[]; extensionPoints?: { id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]; … }` | optional | Plugin capability declarations for interoperability |
4646
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
47-
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: ({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
47+
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: ({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| … +5 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
4848
| **loading** | `{ strategy?: Enum<'eager' \| 'lazy' \| 'parallel' \| 'deferred' \| 'on-demand'>; preload?: object; codeSplitting?: object; dynamicImport?: object; … }` | optional | Plugin loading and runtime behavior configuration |
4949
| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
5050
| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |

content/docs/references/kernel/metadata-plugin.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ const result = MetadataBulkResultSchema.parse(data);
209209
| **executionPinned** | `boolean` | optional | Transaction rows reference a specific version_hash; history GC is disabled and getByHash() MUST resolve old hashes (ADR-0009) |
210210
| **loadOrder** | `integer` | optional | Loading priority (lower = earlier) |
211211
| **domain** | `Enum<'data' \| 'ui' \| 'automation' \| 'system' \| 'security' \| 'ai'>` || Protocol domain |
212-
| **actions** | `{ name: string; label: string; objectName?: string; icon?: string; … }[]` | optional | Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted |
212+
| **actions** | `{ name: string; label: string \| Record<string, string>; objectName?: string; icon?: string; … }[]` | optional | Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted |
213213

214214
### Allowed Values: `MetadataTypeRegistryEntry.type`
215215

0 commit comments

Comments
 (0)