Skip to content

i18n for metadata-type configuration forms (server-side resolver + translation namespace) #1366

@xuyushun441-sys

Description

@xuyushun441-sys

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)

  1. Single source of truth. *.form.ts lives here. Third-party packages that publish new metadata types should ship translations alongside, not patch objectui.
  2. Multi-client reuse. VS Code extension, CLI, future third-party Studios — they all consume getMetaTypes(). Putting i18n in clients duplicates work and drifts.
  3. 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.
  4. 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):

  1. object, field — used on every CRUD type definition
  2. agent, tool, skill — AI builder workflows
  3. flow, workflow, approval — automation
  4. view, page, dashboard, app — UI builder
  5. 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:

  1. Pass Accept-Language header (or ?locale=) on GET /api/v1/meta/types.
  2. Mark the 27 TYPE_LABELS_ZH entries in metadata-admin/i18n.ts as deprecated fallbacks — remove after parity verification.

Acceptance criteria

  • TranslationDataSchema.metadataForms shipped with Zod tests
  • All 18 *.form.ts sections have stable name identifiers
  • resolveMetadataFormLabels() unit-tested for sections, simple fields, composite, repeater, dot-path nested fields
  • getMetaTypes() reads request locale and returns localized form + label
  • packages/metadata/src/translations/en-US.ts (parity with current English) and zh-CN.ts (≥ object/field/agent/flow/view) shipped
  • Backward compat: existing callers without metadataForms see no change
  • Documentation: docs/concepts/metadata-i18n.md (or extend docs/concepts/translations.md)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions