Skip to content

Commit b04a8e3

Browse files
Copilothotlong
andcommitted
refactor(i18n): extract useSafeFieldLabel, eliminate duplication in form/grid
- Extract useSafeFieldLabel to packages/i18n for reuse across plugins - Replace 6 duplicated safe wrappers (5 forms + grid) with shared import - Add BUILTIN_KEYS maintenance documentation - Export useSafeFieldLabel from @object-ui/i18n and @object-ui/react - Update console test mocks to include useSafeFieldLabel Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 64f4422 commit b04a8e3

File tree

12 files changed

+44
-114
lines changed

12 files changed

+44
-114
lines changed

apps/console/src/__tests__/AppSidebar.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ vi.mock('@object-ui/i18n', () => ({
103103
objectDescription: (obj: any) => obj.description,
104104
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
105105
}),
106+
useSafeFieldLabel: () => ({
107+
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
108+
}),
106109
}));
107110

108111
// Mock @object-ui/components to keep most components but simplify some

apps/console/src/__tests__/app-creation-integration.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ vi.mock('@object-ui/i18n', () => ({
273273
objectDescription: (obj: any) => obj.description,
274274
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
275275
}),
276+
useSafeFieldLabel: () => ({
277+
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
278+
}),
276279
}));
277280

278281
describe('Console App Creation Integration', () => {

packages/i18n/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export { createI18n, getDirection, getAvailableLanguages, type I18nConfig, type
3838
export { I18nProvider, useObjectTranslation, useI18nContext, type I18nProviderProps } from './provider';
3939

4040
// Convention-based object/field label i18n
41-
export { useObjectLabel } from './useObjectLabel';
41+
export { useObjectLabel, useSafeFieldLabel } from './useObjectLabel';
4242

4343
// Locale packs
4444
export { builtInLocales, isRTL, RTL_LANGUAGES } from './locales/index';

packages/i18n/src/useObjectLabel.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919

2020
import { useObjectTranslation } from './provider';
2121

22-
/** Built-in Object UI top-level locale keys — not app namespaces. */
22+
/**
23+
* Built-in Object UI top-level locale keys — not app namespaces.
24+
* Update this set when new top-level platform translation keys are added
25+
* to `packages/i18n/src/locales/en.ts` to prevent them from being treated
26+
* as app namespaces during dynamic namespace discovery.
27+
*/
2328
const BUILTIN_KEYS = new Set([
2429
'common', 'validation', 'form', 'table', 'grid', 'calendar',
2530
'list', 'kanban', 'chart', 'dashboard', 'configPanel',
@@ -99,3 +104,19 @@ export function useObjectLabel() {
99104
resolve(`fields.${objectName}.${fieldName}`, fallback),
100105
};
101106
}
107+
108+
/**
109+
* Safe wrapper for useObjectLabel that falls back to identity functions
110+
* when no I18nProvider is available. Suitable for plugin components that
111+
* may be rendered outside an i18n context.
112+
*/
113+
export function useSafeFieldLabel() {
114+
try {
115+
const { fieldLabel } = useObjectLabel();
116+
return { fieldLabel };
117+
} catch {
118+
return {
119+
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
120+
};
121+
}
122+
}

packages/plugin-form/src/DrawerForm.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,10 @@ import {
2424
cn,
2525
} from '@object-ui/components';
2626
import { FormSection } from './FormSection';
27-
import { SchemaRenderer, useObjectLabel } from '@object-ui/react';
27+
import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
2828
import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
2929
import { applyAutoLayout } from './autoLayout';
3030

31-
/**
32-
* Safe wrapper for useObjectLabel that falls back to identity when I18nProvider is unavailable.
33-
*/
34-
function useDrawerFieldLabel() {
35-
try {
36-
const { fieldLabel } = useObjectLabel();
37-
return { fieldLabel };
38-
} catch {
39-
return {
40-
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
41-
};
42-
}
43-
}
44-
4531
/**
4632
* Container-query-based grid classes for form field layout.
4733
* Uses @container / @md: / @2xl: / @4xl: variants so that the grid
@@ -125,7 +111,7 @@ export const DrawerForm: React.FC<DrawerFormProps> = ({
125111
dataSource,
126112
className,
127113
}) => {
128-
const { fieldLabel } = useDrawerFieldLabel();
114+
const { fieldLabel } = useSafeFieldLabel();
129115
const [objectSchema, setObjectSchema] = useState<any>(null);
130116
const [formFields, setFormFields] = useState<FormField[]>([]);
131117
const [formData, setFormData] = useState<Record<string, any>>({});

packages/plugin-form/src/ModalForm.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,10 @@ import {
2727
} from '@object-ui/components';
2828
import { Loader2 } from 'lucide-react';
2929
import { FormSection } from './FormSection';
30-
import { SchemaRenderer, useObjectLabel } from '@object-ui/react';
30+
import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
3131
import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
3232
import { applyAutoLayout, inferModalSize } from './autoLayout';
3333

34-
/**
35-
* Safe wrapper for useObjectLabel that falls back to identity when I18nProvider is unavailable.
36-
*/
37-
function useModalFieldLabel() {
38-
try {
39-
const { fieldLabel } = useObjectLabel();
40-
return { fieldLabel };
41-
} catch {
42-
return {
43-
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
44-
};
45-
}
46-
}
47-
4834
export interface ModalFormSectionConfig {
4935
name?: string;
5036
label?: string;
@@ -137,7 +123,7 @@ export const ModalForm: React.FC<ModalFormProps> = ({
137123
dataSource,
138124
className,
139125
}) => {
140-
const { fieldLabel } = useModalFieldLabel();
126+
const { fieldLabel } = useSafeFieldLabel();
141127
const [objectSchema, setObjectSchema] = useState<any>(null);
142128
const [formFields, setFormFields] = useState<FormField[]>([]);
143129
const [formData, setFormData] = useState<Record<string, any>>({});

packages/plugin-form/src/ObjectForm.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import React, { useEffect, useState, useCallback } from 'react';
1717
import type { ObjectFormSchema, FormField, FormSchema, DataSource } from '@object-ui/types';
18-
import { SchemaRenderer, useObjectLabel } from '@object-ui/react';
18+
import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
1919
import { mapFieldTypeToFormType, buildValidationRules, evaluateCondition, formatFileSize } from '@object-ui/fields';
2020
import { TabbedForm } from './TabbedForm';
2121
import { WizardForm } from './WizardForm';
@@ -25,20 +25,6 @@ import { ModalForm } from './ModalForm';
2525
import { FormSection } from './FormSection';
2626
import { applyAutoLayout } from './autoLayout';
2727

28-
/**
29-
* Safe wrapper for useObjectLabel that falls back to identity when I18nProvider is unavailable.
30-
*/
31-
function useFormFieldLabel() {
32-
try {
33-
const { fieldLabel } = useObjectLabel();
34-
return { fieldLabel };
35-
} catch {
36-
return {
37-
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
38-
};
39-
}
40-
}
41-
4228
export interface ObjectFormProps {
4329
/**
4430
* The schema configuration for the form
@@ -216,7 +202,7 @@ const SimpleObjectForm: React.FC<ObjectFormProps> = ({
216202
schema,
217203
dataSource,
218204
}) => {
219-
const { fieldLabel } = useFormFieldLabel();
205+
const { fieldLabel } = useSafeFieldLabel();
220206

221207
const [objectSchema, setObjectSchema] = useState<any>(null);
222208
const [formFields, setFormFields] = useState<FormField[]>([]);

packages/plugin-form/src/SplitForm.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,9 @@ import {
2323
cn,
2424
} from '@object-ui/components';
2525
import { FormSection } from './FormSection';
26-
import { SchemaRenderer, useObjectLabel } from '@object-ui/react';
26+
import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
2727
import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
2828

29-
/**
30-
* Safe wrapper for useObjectLabel that falls back to identity when I18nProvider is unavailable.
31-
*/
32-
function useSplitFieldLabel() {
33-
try {
34-
const { fieldLabel } = useObjectLabel();
35-
return { fieldLabel };
36-
} catch {
37-
return {
38-
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
39-
};
40-
}
41-
}
42-
4329
export interface SplitFormSectionConfig {
4430
name?: string;
4531
label?: string;
@@ -99,7 +85,7 @@ export const SplitForm: React.FC<SplitFormProps> = ({
9985
dataSource,
10086
className,
10187
}) => {
102-
const { fieldLabel } = useSplitFieldLabel();
88+
const { fieldLabel } = useSafeFieldLabel();
10389
const [objectSchema, setObjectSchema] = useState<any>(null);
10490
const [formData, setFormData] = useState<Record<string, any>>({});
10591
const [loading, setLoading] = useState(true);

packages/plugin-form/src/TabbedForm.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,9 @@ import React, { useState, useCallback } from 'react';
1717
import type { FormField, DataSource } from '@object-ui/types';
1818
import { Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@object-ui/components';
1919
import { FormSection } from './FormSection';
20-
import { SchemaRenderer, useObjectLabel } from '@object-ui/react';
20+
import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
2121
import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
2222

23-
/**
24-
* Safe wrapper for useObjectLabel that falls back to identity when I18nProvider is unavailable.
25-
*/
26-
function useTabbedFieldLabel() {
27-
try {
28-
const { fieldLabel } = useObjectLabel();
29-
return { fieldLabel };
30-
} catch {
31-
return {
32-
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
33-
};
34-
}
35-
}
36-
3723
export interface FormSectionConfig {
3824
/**
3925
* Section identifier (used as tab value)
@@ -180,7 +166,7 @@ export const TabbedForm: React.FC<TabbedFormProps> = ({
180166
dataSource,
181167
className,
182168
}) => {
183-
const { fieldLabel } = useTabbedFieldLabel();
169+
const { fieldLabel } = useSafeFieldLabel();
184170
const [objectSchema, setObjectSchema] = useState<any>(null);
185171
const [formData, setFormData] = useState<Record<string, any>>({});
186172
const [loading, setLoading] = useState(true);

packages/plugin-form/src/WizardForm.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,10 @@ import type { FormField, DataSource } from '@object-ui/types';
1818
import { Button, cn } from '@object-ui/components';
1919
import { Check, ChevronLeft, ChevronRight } from 'lucide-react';
2020
import { FormSection } from './FormSection';
21-
import { SchemaRenderer, useObjectLabel } from '@object-ui/react';
21+
import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
2222
import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields';
2323
import type { FormSectionConfig } from './TabbedForm';
2424

25-
/**
26-
* Safe wrapper for useObjectLabel that falls back to identity when I18nProvider is unavailable.
27-
*/
28-
function useWizardFieldLabel() {
29-
try {
30-
const { fieldLabel } = useObjectLabel();
31-
return { fieldLabel };
32-
} catch {
33-
return {
34-
fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
35-
};
36-
}
37-
}
38-
3925
export interface WizardFormSchema {
4026
type: 'object-form';
4127
formType: 'wizard';
@@ -167,7 +153,7 @@ export const WizardForm: React.FC<WizardFormProps> = ({
167153
dataSource,
168154
className,
169155
}) => {
170-
const { fieldLabel } = useWizardFieldLabel();
156+
const { fieldLabel } = useSafeFieldLabel();
171157
const [objectSchema, setObjectSchema] = useState<any>(null);
172158
const [formData, setFormData] = useState<Record<string, any>>({});
173159
const [loading, setLoading] = useState(true);

0 commit comments

Comments
 (0)