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
10 changes: 10 additions & 0 deletions .changeset/25922-studio-i18n-inspectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@object-ui/i18n': patch
---

feat(i18n): the Studio inspectors' page block, action, dataset, flow node, app navigation and dashboard widget strings move into the `studio` namespace

Clause-②: no
Metadata-protocol: none

- **Second slice of the move ruled on objectstack#25922** (6431032267, Q2 = A; kept by 6431234244). The 520 keys under `engine.inspector.pageBlock.*`, `engine.inspector.action.*`, `engine.inspector.dataset.*`, `engine.inspector.flowNode.*`, `engine.inspector.appNav.*` and `engine.inspector.widget.*` now live in `@object-ui/i18n/namespaces/studio` instead of `@object-ui/app-shell`'s private table, text unchanged in both `en` and `zh`. Studio reads the same strings; a Chinese viewer sees these labels in English for the moment the `zh` catalogue takes to load, as with the first slice. No export changes.
1,146 changes: 0 additions & 1,146 deletions ui/packages/app-shell/src/views/metadata-admin/i18n.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ vi.mock('../previews/useDatasetCatalog', () => ({
}));

import { DashboardWidgetInspector } from './DashboardWidgetInspector';
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ import { registerDefaultMetadataSchemas } from '../default-schemas';
import { registerBuiltinPreviews } from '../previews';
import { registerBuiltinInspectors } from '.';
import { DatasetDefaultInspector, objectChangePatch } from './DatasetDefaultInspector';
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

const DATASET_JSON_SCHEMA = z.toJSONSchema(getMetadataTypeSchema('dataset') as z.ZodType, {
io: 'input',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import { FlowNodeInspector } from './FlowNodeInspector';
import { encodeNestedNodeId } from './flow-nested-selection';
import { t, tFormat, type SupportedLocale } from '../i18n';
import type { MetadataSelection } from '../preview-registry';
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const state = vi.hoisted(() => ({
vi.mock('../useMetadata', () => ({ useMetadataClient: () => state.metadataClient }));

import { PageBlockInspector } from './PageBlockInspector';
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ vi.mock('../useMetadata', () => ({
}));

import { PageBlockInspector } from './PageBlockInspector';
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ vi.mock('../useMetadata', () => ({
}));

import { PageBlockInspector } from './PageBlockInspector';
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ import { SchemaRenderer, PredicateScopeProvider } from '@object-ui/react';
import { PageBlockInspector } from './PageBlockInspector';
import type { MetadataInspectorProps } from '../inspector-registry';
import type { SupportedLocale } from '../i18n';
// The inspector's `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

afterEach(cleanup);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* objectstack#25922 slice S2 — the page block, action, dataset, flow node, app
* navigation and dashboard widget inspector rows (`engine.inspector.{pageBlock,
* action,dataset,flowNode,appNav,widget}.*`) live in `@object-ui/i18n`'s
* `studio` namespace, and app-shell's `t()` still answers every one of them the
* way it did when they sat in its own table.
*
* What would break it:
* - a row copied back into the app-shell table with a different value — the
* table is asked first, so it would shadow the namespace silently;
* - the namespace fallback dropped from `lookupString` — every moved key
* would echo back as itself;
* - a family emptied out of the namespace, which every per-key assertion
* below would then pass over nothing. The floors are per FAMILY, by name,
* so one family draining cannot hide behind another one growing.
*
* The oracle is the namespace's own catalogues, read through
* `loadStudioCatalogue` (the module's public door), not a copy of their text.
*/
import { describe, expect, it } from 'vitest';
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
import { t } from '../i18n';

const EN = await loadStudioCatalogue('en');
// Resident before any case, so the zh lookups below read synchronously.
const ZH = await loadStudioCatalogue('zh');

/** Family → the fewest keys it may carry (measured at the move, rounded down). */
const S2_FAMILY_FLOOR: Readonly<Record<string, number>> = Object.freeze({
pageBlock: 160,
action: 120,
dataset: 80,
flowNode: 40,
appNav: 40,
widget: 40,
});

const familyKeys = (family: string): string[] =>
Object.keys(EN).filter((key) => key.startsWith(`engine.inspector.${family}.`));

describe('S2 inspector rows answer from the studio namespace (objectstack#25922)', () => {
it.each(Object.entries(S2_FAMILY_FLOOR))('%s: the namespace carries the family, in en and zh', (family, floor) => {
const keys = familyKeys(family);
expect(keys.length).toBeGreaterThanOrEqual(floor);
expect(keys.filter((key) => !(key in ZH))).toEqual([]);
});

it.each(Object.keys(S2_FAMILY_FLOOR))('%s: t() answers every key with the namespace value', (family) => {
const wrong = familyKeys(family).flatMap((key) => {
const out: string[] = [];
if (t(key, 'en-US') !== EN[key]) out.push(`en-US ${key}: ${t(key, 'en-US')}`);
if (t(key, 'zh-CN') !== ZH[key]) out.push(`zh-CN ${key}: ${t(key, 'zh-CN')}`);
// A language the namespace has no catalogue for reads English, as it did
// from the app-shell table.
if (t(key, 'de-DE') !== EN[key]) out.push(`de-DE ${key}: ${t(key, 'de-DE')}`);
return out;
});
expect(wrong).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ import path from 'node:path';
import { BLOCK_CONFIG, type BlockPropField, type PlaceholderSpec } from '../block-config';
import { t } from '../../i18n';
import { uiRoot } from '../../../../../../../scripts/ui-root.mjs';
// `engine.inspector.pageBlock.*` lives in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

const PREFIX = 'engine.inspector.pageBlock';

Expand Down Expand Up @@ -474,7 +479,8 @@ describe('placeholders that must NOT be translated (#3979)', () => {
* must LEAVE both tables — and until this describe nothing measured it. The two
* repo-wide i18n gates cannot: `check:i18n-keys` and `check:i18n-dead-keys`
* both read `packages/i18n/src/locales/`, and `engine.inspector.pageBlock.*`
* lives only in `metadata-admin/i18n.ts` (which says so in its own header). So
* lives only in the designer strings (the `studio` namespace since
* objectstack#25922; `metadata-admin/i18n.ts` says so in its own header). So
* each of the four renames so far (objectui#3829, objectui#5212, objectui#8279,
* objectui#8278) shipped with a HAND-WRITTEN pin naming the one key it retired
* — a convention, not an instrument, and the fifth rename is one forgotten pin
Expand All @@ -498,6 +504,11 @@ describe('placeholders that must NOT be translated (#3979)', () => {
* of the source, the same way `PageBlockInspector.i18n.test.tsx` reads its
* own chrome key list rather than hand-copying it.
*
* Since objectstack#25922 slice S2 the prefix lives in the `studio` namespace
* of `@object-ui/i18n` (`packages/i18n/src/namespaces/studio/{en,zh}.ts`), so
* a locale's key set is read from BOTH homes — the app-shell table and the
* namespace catalogue — and a key is retired only when it has left both.
*
* Measured on this checkout: every one of the 171 `engine.inspector.pageBlock.*`
* keys in each table is covered by one of those two, and no key is constructed
* dynamically anywhere (`grep` for a template head under this prefix returns
Expand Down Expand Up @@ -540,6 +551,11 @@ const I18N_SOURCE_PATH = path.join(
ROOT,
'packages/app-shell/src/views/metadata-admin/i18n.ts',
);
/** The `studio` namespace's catalogues — the prefix's home since objectstack#25922 S2. */
const STUDIO_HOME_PATH = {
en: path.join(ROOT, 'packages/i18n/src/namespaces/studio/en.ts'),
zh: path.join(ROOT, 'packages/i18n/src/namespaces/studio/zh.ts'),
};

interface EngineStringTable {
/** The `const` name, for assertion messages. */
Expand All @@ -565,7 +581,7 @@ interface EngineStringTable {
function engineStringTable(source: string, name: string): EngineStringTable {
const lines = source.split('\n');
const open = lines.findIndex((l) => l.startsWith(`const ${name}: Record<string, string> = {`));
if (open === -1) throw new Error(`${name}: declaration not found in i18n.ts — the extractor is stale`);
if (open === -1) throw new Error(`${name}: declaration not found — the extractor is stale`);
const close = lines.findIndex((l, i) => i > open && l === '};');
if (close === -1) throw new Error(`${name}: no column-0 '};' closes the table — the extractor is stale`);
const keys: string[] = [];
Expand Down Expand Up @@ -602,6 +618,7 @@ function shippedLiteralKeys(): Map<string, string[]> {
if (!/\.(ts|tsx|js|jsx|mjs)$/.test(entry.name)) continue;
if (/\.(test|spec)\.[tj]sx?$/.test(entry.name)) continue;
if (full === I18N_SOURCE_PATH) continue; // the DEFINITION site, not a reader
if (full === STUDIO_HOME_PATH.en || full === STUDIO_HOME_PATH.zh) continue; // likewise
const content = readFileSync(full, 'utf8');
if (!content.includes(PREFIX)) continue;
for (const m of content.matchAll(/['"`](engine\.inspector\.pageBlock\.[A-Za-z0-9_.:$-]+)['"`]/g)) {
Expand All @@ -619,6 +636,15 @@ function shippedLiteralKeys(): Map<string, string[]> {
const I18N_SOURCE = readFileSync(I18N_SOURCE_PATH, 'utf8');
const EN_TABLE = engineStringTable(I18N_SOURCE, 'ENGINE_STRINGS_EN');
const ZH_TABLE = engineStringTable(I18N_SOURCE, 'ENGINE_STRINGS_ZH');
const EN_HOME = engineStringTable(readFileSync(STUDIO_HOME_PATH.en, 'utf8'), 'STUDIO_EN');
const ZH_HOME = engineStringTable(readFileSync(STUDIO_HOME_PATH.zh, 'utf8'), 'STUDIO_ZH');

/** One locale's keys across both homes: the app-shell table and the namespace. */
function bothHomes(table: EngineStringTable, home: EngineStringTable): EngineStringTable {
return { name: `${table.name} + ${home.name}`, keys: [...table.keys, ...home.keys], from: table.from, to: table.to };
}
const EN_KEYS = bothHomes(EN_TABLE, EN_HOME);
const ZH_KEYS = bothHomes(ZH_TABLE, ZH_HOME);

/** The keys `SITES` implies — the complete BLOCK_CONFIG-derived vocabulary. */
const DERIVED = new Set(SITES.map((s) => s.derived));
Expand Down Expand Up @@ -656,15 +682,21 @@ describe('a retired key leaves BOTH locale tables (objectui#8368)', () => {
// set arithmetic below would hide it.
expect(EN_TABLE.keys.length).toBe(new Set(EN_TABLE.keys).size);
expect(ZH_TABLE.keys.length).toBe(new Set(ZH_TABLE.keys).size);
// The namespace homes are read the same way, and a key in both homes of
// one locale would be counted twice below.
expect(EN_HOME.keys.length).toBeGreaterThan(150);
expect(ZH_HOME.keys.length).toBeGreaterThan(150);
expect(EN_KEYS.keys.length).toBe(new Set(EN_KEYS.keys).size);
expect(ZH_KEYS.keys.length).toBe(new Set(ZH_KEYS.keys).size);
});

it('the extractor really sees the keys the derivation implies', () => {
// The strongest non-vacuity available: two INDEPENDENT channels for the
// same fact. `SITES` comes from walking BLOCK_CONFIG; these sets come from
// parsing the source text. A parser that silently stopped matching (a
// reformat, a changed indent) fails here by name, not by going quiet.
const enSet = new Set(EN_TABLE.keys);
const zhSet = new Set(ZH_TABLE.keys);
const enSet = new Set(EN_KEYS.keys);
const zhSet = new Set(ZH_KEYS.keys);
expect([...DERIVED].filter((k) => !enSet.has(k))).toEqual([]);
expect([...DERIVED].filter((k) => !zhSet.has(k))).toEqual([]);
expect(DERIVED.size).toBeGreaterThan(150);
Expand Down Expand Up @@ -693,8 +725,8 @@ describe('a retired key leaves BOTH locale tables (objectui#8368)', () => {
// name and the fix is to teach the reader set about it, not to delete the
// key. Loud is the correct direction to be wrong in.
expect(
unreachable(EN_TABLE),
'ENGINE_STRINGS_EN keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
unreachable(EN_KEYS),
'en keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
'shipped source asks for — delete them, or teach the reader set about the new reader',
).toEqual([]);
});
Expand All @@ -704,19 +736,19 @@ describe('a retired key leaves BOTH locale tables (objectui#8368)', () => {
// sibling `every key resolves in zh-CN` cannot see it — a LEFTOVER key
// resolves perfectly well.
expect(
unreachable(ZH_TABLE),
'ENGINE_STRINGS_ZH keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
unreachable(ZH_KEYS),
'zh keys under this prefix that BLOCK_CONFIG no longer implies and no ' +
'shipped source asks for — the half a rename forgets',
).toEqual([]);
});

it('both tables hold the same pageBlock key set', () => {
// "Left BOTH tables", stated directly. Asymmetry here is the signature of a
// half-applied retirement, and the message names which side kept the key.
const en = new Set(pageBlockKeys(EN_TABLE));
const zh = new Set(pageBlockKeys(ZH_TABLE));
expect([...en].filter((k) => !zh.has(k)), 'in ENGINE_STRINGS_EN but not ZH').toEqual([]);
expect([...zh].filter((k) => !en.has(k)), 'in ENGINE_STRINGS_ZH but not EN').toEqual([]);
const en = new Set(pageBlockKeys(EN_KEYS));
const zh = new Set(pageBlockKeys(ZH_KEYS));
expect([...en].filter((k) => !zh.has(k)), 'in the en homes but not zh').toEqual([]);
expect([...zh].filter((k) => !en.has(k)), 'in the zh homes but not en').toEqual([]);
expect(en.size).toBeGreaterThan(150);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ import {
} from '../block-config';
import { BLOCK_TYPE_META, PALETTE_EXCLUSIONS, TYPES_BY_CATEGORY } from '../block-types';
import { t } from '../../i18n';
// `engine.inspector.pageBlock.*` lives in `@object-ui/i18n`'s `studio`
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
// Resident before any case, so the zh rows below read synchronously.
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
await loadStudioCatalogue('zh');

describe('block-config', () => {
it('exposes a configurable panel for every content block with authorable props', () => {
Expand Down Expand Up @@ -800,8 +805,9 @@ describe('object-kanban — the `groupBy` control, and the retired `groupField`

// The two repo-wide i18n gates do NOT reach this table — `check:i18n-keys`
// and `check:i18n-dead-keys` both read `packages/i18n/src/locales/en.ts`, and
// `engine.inspector.pageBlock.*` lives only in `metadata-admin/i18n.ts`
// (which says so in its own header). So the renamed key's two halves are
// `engine.inspector.pageBlock.*` lives only in the designer strings (the
// `studio` namespace since objectstack#25922; `metadata-admin/i18n.ts` says so
// in its own header). So the renamed key's two halves are
// pinned here, in the same file as the field it labels: the sibling
// `block-config-i18n.test.ts` derives the NEW key from this table's position
// and demands both locales define it, and this pin is the other direction —
Expand Down
Loading
Loading