From d3c004dc41d66eee6f0f96670162ccaf2566f20f Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 8 Oct 2026 01:38:27 +0000
Subject: [PATCH 1/2] feat(app-shell): the field inspector says when a
translation overrides the label being edited
Studio's field inspector edits a field's source label, while the Data
pillar's grid headers and form canvas show the label resolved through
@object-ui/i18n's useSafeFieldLabel().fieldLabel, where a translation for the
active language wins. A muted line under the Label input now names the label
the field is shown with and the language, read through that same resolver,
whenever it differs from the input's current value.
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude
---
.../src/views/metadata-admin/i18n.ts | 4 +
...dInspector.labelTranslation-11782.test.tsx | 151 ++++++++++++++++++
.../inspectors/ObjectFieldInspector.tsx | 56 +++++++
3 files changed, 211 insertions(+)
create mode 100644 packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.labelTranslation-11782.test.tsx
diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts
index c00463fb1d..f5804581f1 100644
--- a/packages/app-shell/src/views/metadata-admin/i18n.ts
+++ b/packages/app-shell/src/views/metadata-admin/i18n.ts
@@ -2536,6 +2536,8 @@ const ENGINE_STRINGS_EN: Record = {
'designer.field.section.options': '{type} options',
'designer.field.apiName': 'API name',
'designer.field.label': 'Label',
+ // objectui#11782 — under the Label input: the label a translation shows instead.
+ 'designer.field.labelTranslated': 'Shown as “{label}” in {language}: the app’s translation bundle overrides this label.',
'designer.field.type': 'Type',
'designer.field.required': 'Required',
'designer.field.unique': 'Unique',
@@ -5516,6 +5518,8 @@ const ENGINE_STRINGS_ZH: Record = {
'designer.field.section.options': '{type} 选项',
'designer.field.apiName': 'API 名称',
'designer.field.label': '显示名',
+ // objectui#11782.
+ 'designer.field.labelTranslated': '在 {language} 中显示为“{label}”:应用的翻译包覆盖了此显示名。',
'designer.field.type': '类型',
'designer.field.required': '必填',
'designer.field.unique': '唯一',
diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.labelTranslation-11782.test.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.labelTranslation-11782.test.tsx
new file mode 100644
index 0000000000..fb5fdf2c8d
--- /dev/null
+++ b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.labelTranslation-11782.test.tsx
@@ -0,0 +1,151 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import * as React from 'react';
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { render, screen, fireEvent, cleanup } from '@testing-library/react';
+import { I18nProvider, createI18n } from '@object-ui/i18n';
+
+// The inspector loads the object list and the picklist roster on mount; stub
+// both, as the other inspector suites do.
+vi.mock('../useMetadata', () => ({
+ useMetadataClient: () => ({
+ list: vi.fn().mockResolvedValue([]),
+ listDrafts: vi.fn().mockResolvedValue([]),
+ }),
+}));
+
+vi.mock('../previews/useObjectFields', () => ({
+ useObjectFields: () => ({ fields: [], loading: false, error: null }),
+}));
+
+import { ObjectFieldInspector } from './ObjectFieldInspector';
+import { tFormat, type SupportedLocale } from '../i18n';
+
+afterEach(cleanup);
+
+const HINT = 'field-label-translation-hint';
+
+/**
+ * The runtime bundle shape `transformSpecTranslations` produces from an app's
+ * authored `objects..fields..label` entries: field labels are
+ * flattened to `.fields..`. This is what the console's
+ * `loadLanguage` installs, and what the Data pillar's grid and form canvas
+ * resolve a field's label from (`useSafeFieldLabel().fieldLabel`).
+ */
+function appBundle(fields: Record) {
+ return { showcase: { fields: { showcase_account: fields } } };
+}
+
+function renderInspector({
+ language,
+ locale = 'en-US',
+ bundle,
+ label = 'Tax ID (EIN)',
+}: {
+ language: string;
+ locale?: SupportedLocale;
+ bundle: Record;
+ label?: string;
+}) {
+ const onPatch = vi.fn();
+ const instance = createI18n({
+ defaultLanguage: language,
+ detectBrowserLanguage: false,
+ resources: { [language]: appBundle(bundle) },
+ });
+ const ui = (fieldLabel: string) => (
+
+
+
+ );
+ const utils = render(ui(label));
+ return { onPatch, rerenderWithLabel: (next: string) => utils.rerender(ui(next)), ...utils };
+}
+
+const labelInput = () => screen.getByTestId('field-label-input') as HTMLInputElement;
+
+describe('ObjectFieldInspector — the Label input says when a translation overrides it (objectui#11782)', () => {
+ it('names the translated label and the language it is shown in', () => {
+ renderInspector({ language: 'en', bundle: { tax_id: 'Tax ID' } });
+
+ const hint = screen.getByTestId(HINT);
+ expect(hint.textContent).toBe(
+ tFormat('designer.field.labelTranslated', 'en-US', { label: 'Tax ID', language: 'en' }),
+ );
+ // The input still edits the SOURCE label; the hint only reports.
+ expect(labelInput().value).toBe('Tax ID (EIN)');
+ });
+
+ it('names the language the resolver read, not the designer locale it collapsed to', () => {
+ // A ja session reads the designer's en-US table (`useMetadataLocale`
+ // collapses every non-zh language), but the canvas shows the ja bundle.
+ renderInspector({ language: 'ja', locale: 'en-US', bundle: { tax_id: '納税者番号' } });
+
+ expect(screen.getByTestId(HINT).textContent).toBe(
+ tFormat('designer.field.labelTranslated', 'en-US', { label: '納税者番号', language: 'ja' }),
+ );
+ });
+
+ it('reads the zh table in a zh session', () => {
+ renderInspector({ language: 'zh-CN', locale: 'zh-CN', bundle: { tax_id: '税号' } });
+
+ expect(screen.getByTestId(HINT).textContent).toBe(
+ tFormat('designer.field.labelTranslated', 'zh-CN', { label: '税号', language: 'zh-CN' }),
+ );
+ });
+
+ it('shows nothing for a field the bundle does not translate', () => {
+ // Positive control in the same bundle: `website` IS translated, so the
+ // namespace is discovered and the absence is about `tax_id` alone.
+ renderInspector({ language: 'en', bundle: { website: 'Web site' } });
+
+ expect(screen.queryByTestId(HINT)).toBeNull();
+ });
+
+ it('shows nothing when the translation equals the label being edited', () => {
+ renderInspector({ language: 'en', bundle: { tax_id: 'Tax ID (EIN)' } });
+
+ expect(screen.queryByTestId(HINT)).toBeNull();
+ });
+
+ it("compares against the input's current value, not the label the field loaded with", () => {
+ const { rerenderWithLabel } = renderInspector({ language: 'en', bundle: { tax_id: 'Tax ID' } });
+ expect(screen.getByTestId(HINT)).toBeTruthy();
+
+ rerenderWithLabel('Tax ID');
+ expect(screen.queryByTestId(HINT)).toBeNull();
+
+ rerenderWithLabel('Taxpayer ID');
+ expect(screen.getByTestId(HINT).textContent).toBe(
+ tFormat('designer.field.labelTranslated', 'en-US', { label: 'Tax ID', language: 'en' }),
+ );
+ });
+
+ it('commits a typed label to the source `label` exactly as before', () => {
+ const { onPatch } = renderInspector({ language: 'en', bundle: { tax_id: 'Tax ID' } });
+
+ fireEvent.change(labelInput(), { target: { value: 'Employer ID' } });
+
+ expect(onPatch).toHaveBeenCalledTimes(1);
+ const fields = onPatch.mock.calls[0][0].fields as Record>;
+ expect(fields.tax_id).toEqual({ type: 'text', label: 'Employer ID' });
+ // The translation is never written into the draft.
+ expect(JSON.stringify(onPatch.mock.calls[0][0])).not.toContain('"Tax ID"');
+ });
+});
diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx
index 88e9cb5777..432eb3a676 100644
--- a/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx
+++ b/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx
@@ -43,6 +43,7 @@ import {
import { ValueDomainSchema } from '@objectstack/spec/shared';
import type { MetadataInspectorProps } from '../inspector-registry.js';
import { MetadataClient } from '@object-ui/data-objectstack';
+import { useObjectTranslation, useSafeFieldLabel } from '@object-ui/i18n';
import { useMetadataClient } from '../useMetadata.js';
import {
InspectorShell,
@@ -926,6 +927,12 @@ export function ObjectFieldInspector({
disabled={readOnly}
testId="field-label-input"
/>
+
+ {tFormat('designer.field.labelTranslated', locale, { label: translated, language: i18n.language })}
+
+ );
+}
+
/** Type-aware default-value editor. Stores the literal on `Field.defaultValue`. */
function DefaultValueField({
kind,
From 2ec68bc86693a964ae2328079276bb904aca4bad Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 8 Oct 2026 01:38:43 +0000
Subject: [PATCH 2/2] chore(changeset): patch for the field inspector's
translated-label hint
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude
---
.changeset/11782-label-translation-hint.md | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 .changeset/11782-label-translation-hint.md
diff --git a/.changeset/11782-label-translation-hint.md b/.changeset/11782-label-translation-hint.md
new file mode 100644
index 0000000000..42803e5914
--- /dev/null
+++ b/.changeset/11782-label-translation-hint.md
@@ -0,0 +1,11 @@
+---
+'@object-ui/app-shell': patch
+---
+
+Studio's field inspector says when a translation overrides the label being edited (objectui#11782).
+
+The inspector's Label input edits a field's source label. The Data pillar's grid headers and form canvas show the label the app's translation bundle gives the field for the active language, so a translated field read one way on the canvas and another in the input, with nothing explaining why: the showcase's `showcase_account.tax_id` showed "Tax ID" on the canvas and "Tax ID (EIN)" in the input.
+
+A muted line under the Label input now reads, for example, `Shown as “Tax ID” in en: the app’s translation bundle overrides this label.` The label comes from the same resolver the grid and the canvas use (`useSafeFieldLabel().fieldLabel` from `@object-ui/i18n`), and the language named is the one that resolver reads, so a `ja` session is told `ja` even though the designer's own strings are English. The line shows only when the translated label differs from the input's current value: a field with no translation, or one whose translation equals the label being edited, shows nothing. The input still edits and saves the source label exactly as before, and how translations resolve is unchanged.
+
+Nothing is added to the package entry: no export, prop, type member or language-pack key. The new copy lives in the metadata-admin designer's own string tables (en and zh).