Skip to content

Commit 2a2ca18

Browse files
Copilothotlong
andcommitted
refactor(i18n): extract shared createSafeTranslation hook to @object-ui/i18n
- Move safe translation pattern from inline implementations to shared createSafeTranslation factory in @object-ui/i18n - Refactor useFieldTranslation to use createSafeTranslation - Refactor FieldFactory and form.tsx to use createSafeTranslation - Restore fallback placeholder in renderFieldComponent for safety - Update CHANGELOG.md with all i18n improvements Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/1e02cef1-b595-4316-86d4-24d7df245f95
1 parent e3a4404 commit 2a2ca18

6 files changed

Lines changed: 92 additions & 90 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- **Chinese language pack (zh.ts) untranslated key** (`@object-ui/i18n`): Fixed `console.objectView.toolbarEnabledCount` which was still in English (`'{{count}} of {{total}} enabled'`) — now properly translated to `'已启用 {{count}}/{{total}} 项'`. Also fixed the same untranslated key in all other 8 non-English locales (ja, ko, de, fr, es, pt, ru, ar).
13+
14+
- **Hardcoded English strings in platform UI** (`apps/console`, `@object-ui/fields`, `@object-ui/react`, `@object-ui/components`): Replaced hardcoded English strings with i18n `t()` calls:
15+
- Console `App.tsx`: Modal dialog title (`"Create/Edit Contact"`), description (`"Add a new ... to your database."`), submitText (`"Save Record"`), and cancelText (`"Cancel"`) now use i18n keys.
16+
- `SelectField`: Default placeholder `"Select an option"` now uses `t('common.selectOption')`.
17+
- `LookupField`: Default placeholder `"Select..."` and search placeholder `"Search..."` now use i18n keys.
18+
- `FieldFactory`: Default select option placeholder now uses `t('common.selectOption')`.
19+
- Form renderer: Default select placeholder now uses `t('common.selectOption')`.
20+
21+
### Added
22+
23+
- **New i18n keys for select/form components** (`@object-ui/i18n`): Added new translation keys across all 10 locale packs:
24+
- `common.selectOption` — Default select field placeholder (e.g., '请选择' in Chinese)
25+
- `common.select` — Short select placeholder (e.g., '选择...' in Chinese)
26+
- `form.createTitle` — Form dialog create title with interpolation (e.g., '新建{{object}}' in Chinese)
27+
- `form.editTitle` — Form dialog edit title with interpolation
28+
- `form.createDescription` — Form dialog create description with interpolation
29+
- `form.editDescription` — Form dialog edit description with interpolation
30+
- `form.saveRecord` — Save record button text
31+
32+
- **Safe translation hook for field widgets** (`@object-ui/fields`): Added `useFieldTranslation` hook that provides fallback to English defaults when no `I18nProvider` is available, following the same pattern as `useDetailTranslation` in `@object-ui/plugin-detail`.
33+
34+
### Fixed
35+
1236
- **ObjectView ChatterPanel test assertion mismatch** (`apps/console`): Fixed the failing CI test in `ObjectView.test.tsx` where the RecordChatterPanel drawer test used `getByLabelText('Show discussion')` but the actual aria-label rendered by the component is `'Show Discussion (0)'` (from the i18n default translation `detail.showDiscussion` with count parameter). Updated the test assertion to match the correct aria-label.
1337

1438
### Changed

packages/components/src/renderers/form/form.tsx

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,12 @@ import { AlertCircle, Loader2 } from 'lucide-react';
2828
import { cn } from '../../lib/utils';
2929
import React from 'react';
3030
import { SchemaRendererContext } from '@object-ui/react';
31-
import { useObjectTranslation } from '@object-ui/i18n';
31+
import { createSafeTranslation } from '@object-ui/i18n';
3232

33-
/**
34-
* Safe translation helper that falls back to English defaults when
35-
* no I18nProvider is available.
36-
*/
37-
function useSafeFormTranslation() {
38-
try {
39-
const result = useObjectTranslation();
40-
const testValue = result.t('common.selectOption');
41-
if (testValue === 'common.selectOption') {
42-
return { t: (key: string) => {
43-
const defaults: Record<string, string> = {
44-
'common.selectOption': 'Select an option',
45-
};
46-
return defaults[key] || key;
47-
}};
48-
}
49-
return { t: result.t };
50-
} catch {
51-
return { t: (key: string) => {
52-
const defaults: Record<string, string> = {
53-
'common.selectOption': 'Select an option',
54-
};
55-
return defaults[key] || key;
56-
}};
57-
}
58-
}
33+
const useSafeFormTranslation = createSafeTranslation(
34+
{ 'common.selectOption': 'Select an option' },
35+
'common.selectOption',
36+
);
5937

6038
// Form renderer component - Airtable-style feature-complete form
6139
ComponentRegistry.register('form',
@@ -543,7 +521,7 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
543521
return (
544522
<Select value={selectValue} onValueChange={selectOnChange} {...selectProps}>
545523
<SelectTrigger className="min-h-[44px] sm:min-h-0">
546-
<SelectValue placeholder={placeholder} />
524+
<SelectValue placeholder={placeholder || 'Select an option'} />
547525
</SelectTrigger>
548526
<SelectContent>
549527
{options.map((opt: SelectOption) => (

packages/fields/src/widgets/useFieldTranslation.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Safe translation hook for field widgets.
33
* Falls back to English defaults when no I18nProvider is available.
44
*/
5-
import { useObjectTranslation } from '@object-ui/i18n';
5+
import { createSafeTranslation } from '@object-ui/i18n';
66

77
const FIELD_DEFAULTS: Record<string, string> = {
88
'common.selectOption': 'Select an option',
@@ -11,37 +11,7 @@ const FIELD_DEFAULTS: Record<string, string> = {
1111
'table.selected': '{{count}} selected',
1212
};
1313

14-
export function useFieldTranslation() {
15-
try {
16-
const result = useObjectTranslation();
17-
// Test if i18n is properly configured
18-
const testValue = result.t('common.selectOption');
19-
if (testValue === 'common.selectOption') {
20-
// i18n not configured — use defaults
21-
return {
22-
t: (key: string, options?: Record<string, unknown>) => {
23-
let value = FIELD_DEFAULTS[key] || key;
24-
if (options) {
25-
for (const [k, v] of Object.entries(options)) {
26-
value = value.replace(`{{${k}}}`, String(v));
27-
}
28-
}
29-
return value;
30-
},
31-
};
32-
}
33-
return { t: result.t };
34-
} catch {
35-
return {
36-
t: (key: string, options?: Record<string, unknown>) => {
37-
let value = FIELD_DEFAULTS[key] || key;
38-
if (options) {
39-
for (const [k, v] of Object.entries(options)) {
40-
value = value.replace(`{{${k}}}`, String(v));
41-
}
42-
}
43-
return value;
44-
},
45-
};
46-
}
47-
}
14+
export const useFieldTranslation = createSafeTranslation(
15+
FIELD_DEFAULTS,
16+
'common.selectOption',
17+
);

packages/i18n/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export { createI18n, getDirection, getAvailableLanguages, type I18nConfig, type
3737
// React integration
3838
export { I18nProvider, useObjectTranslation, useI18nContext, type I18nProviderProps } from './provider';
3939

40+
// Safe translation hook factory
41+
export { createSafeTranslation } from './useSafeTranslation';
42+
4043
// Convention-based object/field label i18n
4144
export { useObjectLabel, useSafeFieldLabel } from './useObjectLabel';
4245

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Safe translation hook with fallback to default strings.
3+
*
4+
* When no I18nProvider is available (e.g., in tests or standalone usage),
5+
* this hook falls back to the provided default translations instead of
6+
* returning raw i18n keys.
7+
*
8+
* @param defaults - Fallback English translations keyed by i18n key
9+
* @param testKey - A key to test if i18n is properly configured (must be in defaults)
10+
*/
11+
import { useObjectTranslation } from './provider';
12+
13+
export function createSafeTranslation(
14+
defaults: Record<string, string>,
15+
testKey: string,
16+
) {
17+
return function useSafeTranslation() {
18+
try {
19+
const result = useObjectTranslation();
20+
const testValue = result.t(testKey);
21+
if (testValue === testKey) {
22+
return {
23+
t: (key: string, options?: Record<string, unknown>) => {
24+
let value = defaults[key] || key;
25+
if (options) {
26+
for (const [k, v] of Object.entries(options)) {
27+
value = value.replace(`{{${k}}}`, String(v));
28+
}
29+
}
30+
return value;
31+
},
32+
};
33+
}
34+
return { t: result.t };
35+
} catch {
36+
return {
37+
t: (key: string, options?: Record<string, unknown>) => {
38+
let value = defaults[key] || key;
39+
if (options) {
40+
for (const [k, v] of Object.entries(options)) {
41+
value = value.replace(`{{${k}}}`, String(v));
42+
}
43+
}
44+
return value;
45+
},
46+
};
47+
}
48+
};
49+
}

packages/react/src/components/form/FieldFactory.tsx

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,12 @@ import React from 'react';
1010
import { UseFormReturn } from 'react-hook-form';
1111
import type { FormField } from '@objectstack/spec/ui';
1212
import { resolveI18nLabel } from '../../utils/i18n';
13-
import { useObjectTranslation } from '@object-ui/i18n';
13+
import { createSafeTranslation } from '@object-ui/i18n';
1414

15-
/**
16-
* Safe translation helper that falls back to English defaults when
17-
* no I18nProvider is available.
18-
*/
19-
function useSafeTranslation() {
20-
try {
21-
const result = useObjectTranslation();
22-
const testValue = result.t('common.selectOption');
23-
if (testValue === 'common.selectOption') {
24-
return { t: (key: string) => {
25-
const defaults: Record<string, string> = {
26-
'common.selectOption': 'Select an option',
27-
};
28-
return defaults[key] || key;
29-
}};
30-
}
31-
return { t: result.t };
32-
} catch {
33-
return { t: (key: string) => {
34-
const defaults: Record<string, string> = {
35-
'common.selectOption': 'Select an option',
36-
};
37-
return defaults[key] || key;
38-
}};
39-
}
40-
}
15+
const useSafeTranslation = createSafeTranslation(
16+
{ 'common.selectOption': 'Select an option' },
17+
'common.selectOption',
18+
);
4119

4220
/**
4321
* Extended form field with additional properties for complex widgets

0 commit comments

Comments
 (0)