Found while bumping @objectstack/spec to 17.0.0-rc.6 for objectstack#7100. Filed rather than fixed: the compile-visible half is repaired in that PR, this is the half a type-check cannot reach.
What changed upstream
I18nLabel was a plain string through rc.5 and became a union in rc.6:
rc.5 declare const I18nLabelSchema: z.ZodString;
type I18nLabel = z.infer< typeof I18nLabelSchema >; // string
rc.6 declare const I18nLabelSchema: z.ZodUnion< readonly [z.ZodString, z.ZodRecord< z.ZodString, z.ZodString >] >;
type I18nLabel = z.input< typeof I18nLabelSchema >; // string | Record< string, string >
The standalone I18nObject / I18nObjectSchema were retired in the same release and folded into I18nLabel; rc.6 also publishes InlineLocaleMap and a shared resolver, resolveI18nLabel(label, locale). So from rc.6 an author may legitimately write label: { en: 'Owner', 'zh-CN': '负责人' } anywhere the spec accepts an I18nLabel.
Part 1 — the reads the compiler cannot see
Bumping the pin turned up 9 sites where an I18nLabel flows into a string or ReactNode slot, and the compiler named every one of them. Those are fixed. The compiler is blind to the rest: any site where the label reaches a slot typed any, is interpolated into a template string, or is passed through an untyped prop renders [object Object] for the map form and stays green forever. That is the exact harm rc.6's own resolver doc comment calls out.
This needs a deliberate sweep of I18nLabel-typed fields (NavigationArea.label, DashboardWidget.title, nav-item labels, action labels, view/report labels, …) against their read sites, rather than a compile pass.
Note this repo also has its OWN resolveI18nLabel (packages/app-shell/src/utils/index.ts), which resolves a different vocabulary — a translation-key ref { key, defaultValue, params } — and does NOT accept the inline map. Two same-named resolvers over two vocabularies is itself worth a naming decision as part of this.
Part 2 — Studio cannot author the map form
DashboardWidgetInspector's title editor is a single-line Input bound straight to widget.title. Resolving a map into it and writing e.target.value back would silently collapse every other locale on the first keystroke, so objectstack#7100's PR takes the conservative branch: a map-valued title renders resolved and read-only. Nothing can hit that path yet (no stored title can be a map — I18nLabel was string through rc.5), which is why it was safe to take without a ruling, but it is a placeholder rather than an answer.
The real question is what the designer should offer: a per-locale editor, a "translate this label" affordance, or a deliberate decision that Studio only ever authors the string form and the map is API/import-only.
Why not in objectstack#7100's PR
That card is the global_nav retirement; it carries the rc.6 bump only because the two cannot compile apart. Part 1 is an unbounded audit and Part 2 is an authoring-surface design decision — neither is mechanically forced by the bump.
Found while bumping
@objectstack/specto17.0.0-rc.6for objectstack#7100. Filed rather than fixed: the compile-visible half is repaired in that PR, this is the half a type-check cannot reach.What changed upstream
I18nLabelwas a plain string through rc.5 and became a union in rc.6:The standalone
I18nObject/I18nObjectSchemawere retired in the same release and folded intoI18nLabel; rc.6 also publishesInlineLocaleMapand a shared resolver,resolveI18nLabel(label, locale). So from rc.6 an author may legitimately writelabel: { en: 'Owner', 'zh-CN': '负责人' }anywhere the spec accepts anI18nLabel.Part 1 — the reads the compiler cannot see
Bumping the pin turned up 9 sites where an
I18nLabelflows into astringorReactNodeslot, and the compiler named every one of them. Those are fixed. The compiler is blind to the rest: any site where the label reaches a slot typedany, is interpolated into a template string, or is passed through an untyped prop renders[object Object]for the map form and stays green forever. That is the exact harm rc.6's own resolver doc comment calls out.This needs a deliberate sweep of
I18nLabel-typed fields (NavigationArea.label,DashboardWidget.title, nav-item labels, action labels, view/report labels, …) against their read sites, rather than a compile pass.Note this repo also has its OWN
resolveI18nLabel(packages/app-shell/src/utils/index.ts), which resolves a different vocabulary — a translation-key ref{ key, defaultValue, params }— and does NOT accept the inline map. Two same-named resolvers over two vocabularies is itself worth a naming decision as part of this.Part 2 — Studio cannot author the map form
DashboardWidgetInspector's title editor is a single-lineInputbound straight towidget.title. Resolving a map into it and writinge.target.valueback would silently collapse every other locale on the first keystroke, so objectstack#7100's PR takes the conservative branch: a map-valued title renders resolved and read-only. Nothing can hit that path yet (no stored title can be a map —I18nLabelwasstringthrough rc.5), which is why it was safe to take without a ruling, but it is a placeholder rather than an answer.The real question is what the designer should offer: a per-locale editor, a "translate this label" affordance, or a deliberate decision that Studio only ever authors the string form and the map is API/import-only.
Why not in objectstack#7100's PR
That card is the
global_navretirement; it carries the rc.6 bump only because the two cannot compile apart. Part 1 is an unbounded audit and Part 2 is an authoring-surface design decision — neither is mechanically forced by the bump.