Background
The metadata-admin engine in objectui (apps/studio/metadata/<type>/<name>) renders edit forms for all 18 metadata types via the entry.form payload that ships from getMetaTypes() (packages/objectql/src/protocol.ts). The form layouts come from canonical defineForm() definitions in packages/spec/src/<domain>/*.form.ts:
// packages/spec/src/data/object.form.ts
export const objectForm = defineForm({
sections: [
{ label: 'Basics', fields: [
{ field: 'name', helpText: 'snake_case unique identifier (immutable after creation)' },
{ field: 'label', helpText: 'Singular display name (e.g. "Account")' },
…
]},
{ label: 'Capabilities', collapsible: true, fields: [ … ] },
],
});
All label / helpText / placeholder / description strings are hardcoded English plain strings. No translation pipeline exists for them — they bypass the i18n machinery that the rest of the platform already uses for business objects.
Current State Audit
| Layer |
File |
i18n today |
Type display name ("Object" / "对象") |
packages/spec/src/kernel/metadata-plugin.zod.ts DEFAULT_METADATA_TYPE_REGISTRY |
Server has English-only label: 'Object'. objectui currently hardcodes the 27 zh-CN translations as a client-side fallback (packages/app-shell/src/views/metadata-admin/i18n.ts). |
| Form section/field labels & helpText |
packages/spec/src/<domain>/*.form.ts × 18 files (objectForm, fieldForm, agentForm, flowForm, …) |
None. Bare English strings, transparently passed through. |
| Runtime business-object forms |
packages/spec/src/system/translation.zod.ts TranslationDataSchema |
✅ Fully wired: objects.<obj>._sections.<s>.label, objects.<obj>.fields.<f>.{label,help}, _views, _actions. |
I18nLabelSchema |
packages/spec/src/ui/i18n.zod.ts |
Comment says "translations are managed through translation files", but there is no resolver for the metadata-form path. |
The asymmetry: a user-defined account object gets full i18n via ObjectTranslationDataSchema, but the object metadata type's own configuration form (the form you use to define an object) does not.
Why server-side (this repo)
- Single source of truth.
*.form.ts lives here. Third-party packages that publish new metadata types should ship translations alongside, not patch objectui.
- Multi-client reuse. VS Code extension, CLI, future third-party Studios — they all consume
getMetaTypes(). Putting i18n in clients duplicates work and drifts.
- Architectural consistency. Runtime business objects already use
objects.<x>.fields.<y>.label. Metadata is self-describing; the form for editing a field is conceptually the same as the form for editing an account. They deserve the same i18n path.
- Runtime locale switching just works. Server renders per request
Accept-Language — no client logic.
Proposed Design
1. Extend TranslationDataSchema — add metadataForms namespace
packages/spec/src/system/translation.zod.ts:
metadataForms: z.record(z.string(), z.object({
/** Override DEFAULT_METADATA_TYPE_REGISTRY label */
label: z.string().optional(),
description: z.string().optional(),
/** Section overrides, keyed by stable section.name */
sections: z.record(z.string(), z.object({
label: z.string().optional(),
description: z.string().optional(),
})).optional(),
/**
* Field overrides, keyed by field path.
* Dot-notation supported for nested composite/repeater fields:
* "name"
* "capabilities.trackHistory"
* "fields.items.label" (repeater "fields" → row → "label")
*/
fields: z.record(z.string(), z.object({
label: z.string().optional(),
helpText: z.string().optional(),
placeholder: z.string().optional(),
})).optional(),
})).optional().describe('Translations for metadata-type configuration forms (keyed by metadata type)')
2. Give every form section a stable name
Today sections only have label. Add name: 'basics', name: 'capabilities', etc., across all 18 *.form.ts files. The name is the translation key; label becomes the en-US fallback.
FormSectionSchema already exists in packages/spec/src/ui/view.zod.ts — verify it has (or add) name?: string.
3. New resolver in packages/metadata/src/translations/
export function resolveMetadataFormLabels(
form: FormView,
type: string,
bundle: TranslationData | undefined,
): FormView
Walks sections → fields → composite/repeater nested fields, replacing label / description / helpText / placeholder when bundle.metadataForms[type] has an entry. Pure function, no side effects, returns a new form object (don't mutate the cached canonical one).
4. Wire into getMetaTypes()
packages/objectql/src/protocol.ts around L856:
async getMetaTypes() {
const locale = this.request?.context?.locale
?? parseAcceptLanguage(this.request?.headers?.['accept-language'])
?? this.config?.defaultLocale
?? 'en-US';
const bundle = await translationService.getBundle(locale); // already exists for objects
return entries.map(entry => ({
...entry,
label: bundle?.metadataForms?.[entry.type]?.label ?? entry.label,
form: entry.form
? resolveMetadataFormLabels(entry.form, entry.type, bundle)
: undefined,
}));
}
Cache per-locale-per-type-hash to keep the existing perf characteristics.
5. Ship built-in bundles
packages/metadata/src/translations/{en-US,zh-CN}.ts covering all 18 TYPE_TO_FORM entries.
Priority order for zh-CN (cover the high-traffic types first):
object, field — used on every CRUD type definition
agent, tool, skill — AI builder workflows
flow, workflow, approval — automation
view, page, dashboard, app — UI builder
report, action, permission, profile, role, hook, email_template — remainder
Can be authored manually now and later migrated to standard translation tooling.
6. Migration / compatibility
- 100% backward compatible. If
bundle is missing or has no metadataForms entry for a type, the resolver returns the form unchanged (English).
- objectui can immediately consume the new payload — it already renders whatever
entry.form says.
Out of scope (this issue)
- Engine UI strings (button labels like "Save"/"Cancel"/"Edit", tab names) — those live in objectui and can stay client-side. Listed for completeness in
objectui/packages/app-shell/src/views/metadata-admin/i18n.ts.
- Inline
I18nObjectSchema (the { key, defaultValue, params } shape). Not needed for metadata forms; bundle-based lookup is simpler.
Client-side follow-up (objectui)
Tracked separately once this lands:
- Pass
Accept-Language header (or ?locale=) on GET /api/v1/meta/types.
- Mark the 27
TYPE_LABELS_ZH entries in metadata-admin/i18n.ts as deprecated fallbacks — remove after parity verification.
Acceptance criteria
References
Background
The metadata-admin engine in objectui (
apps/studio/metadata/<type>/<name>) renders edit forms for all 18 metadata types via theentry.formpayload that ships fromgetMetaTypes()(packages/objectql/src/protocol.ts). The form layouts come from canonicaldefineForm()definitions inpackages/spec/src/<domain>/*.form.ts:All
label/helpText/placeholder/descriptionstrings are hardcoded English plain strings. No translation pipeline exists for them — they bypass the i18n machinery that the rest of the platform already uses for business objects.Current State Audit
"Object"/"对象")packages/spec/src/kernel/metadata-plugin.zod.tsDEFAULT_METADATA_TYPE_REGISTRYlabel: 'Object'. objectui currently hardcodes the 27 zh-CN translations as a client-side fallback (packages/app-shell/src/views/metadata-admin/i18n.ts).packages/spec/src/<domain>/*.form.ts× 18 files (objectForm,fieldForm,agentForm,flowForm, …)packages/spec/src/system/translation.zod.tsTranslationDataSchemaobjects.<obj>._sections.<s>.label,objects.<obj>.fields.<f>.{label,help},_views,_actions.I18nLabelSchemapackages/spec/src/ui/i18n.zod.tsThe asymmetry: a user-defined
accountobject gets full i18n viaObjectTranslationDataSchema, but theobjectmetadata type's own configuration form (the form you use to define an object) does not.Why server-side (this repo)
*.form.tslives here. Third-party packages that publish new metadata types should ship translations alongside, not patch objectui.getMetaTypes(). Putting i18n in clients duplicates work and drifts.objects.<x>.fields.<y>.label. Metadata is self-describing; the form for editing afieldis conceptually the same as the form for editing anaccount. They deserve the same i18n path.Accept-Language— no client logic.Proposed Design
1. Extend
TranslationDataSchema— addmetadataFormsnamespacepackages/spec/src/system/translation.zod.ts:2. Give every form section a stable
nameToday sections only have
label. Addname: 'basics',name: 'capabilities', etc., across all 18*.form.tsfiles. Thenameis the translation key;labelbecomes the en-US fallback.FormSectionSchemaalready exists inpackages/spec/src/ui/view.zod.ts— verify it has (or add)name?: string.3. New resolver in
packages/metadata/src/translations/Walks sections → fields → composite/repeater nested fields, replacing
label/description/helpText/placeholderwhenbundle.metadataForms[type]has an entry. Pure function, no side effects, returns a new form object (don't mutate the cached canonical one).4. Wire into
getMetaTypes()packages/objectql/src/protocol.tsaround L856:Cache per-locale-per-type-hash to keep the existing perf characteristics.
5. Ship built-in bundles
packages/metadata/src/translations/{en-US,zh-CN}.tscovering all 18TYPE_TO_FORMentries.Priority order for zh-CN (cover the high-traffic types first):
object,field— used on every CRUD type definitionagent,tool,skill— AI builder workflowsflow,workflow,approval— automationview,page,dashboard,app— UI builderreport,action,permission,profile,role,hook,email_template— remainderCan be authored manually now and later migrated to standard translation tooling.
6. Migration / compatibility
bundleis missing or has nometadataFormsentry for a type, the resolver returns the form unchanged (English).entry.formsays.Out of scope (this issue)
objectui/packages/app-shell/src/views/metadata-admin/i18n.ts.I18nObjectSchema(the{ key, defaultValue, params }shape). Not needed for metadata forms; bundle-based lookup is simpler.Client-side follow-up (objectui)
Tracked separately once this lands:
Accept-Languageheader (or?locale=) onGET /api/v1/meta/types.TYPE_LABELS_ZHentries inmetadata-admin/i18n.tsas deprecated fallbacks — remove after parity verification.Acceptance criteria
TranslationDataSchema.metadataFormsshipped with Zod tests*.form.tssections have stablenameidentifiersresolveMetadataFormLabels()unit-tested for sections, simple fields, composite, repeater, dot-path nested fieldsgetMetaTypes()reads request locale and returns localizedform+labelpackages/metadata/src/translations/en-US.ts(parity with current English) andzh-CN.ts(≥ object/field/agent/flow/view) shippedmetadataFormssee no changedocs/concepts/metadata-i18n.md(or extenddocs/concepts/translations.md)References
packages/objectql/src/protocol.tsL856getMetaTypes()packages/spec/src/data/object.form.ts(and 17 siblings)packages/spec/src/system/translation.zod.tspackages/spec/src/ui/i18n.zod.ts