Skip to content

Commit a06a1c7

Browse files
authored
Merge pull request #999 from objectstack-ai/copilot/fix-internationalization-issues
2 parents b8d65c5 + f9a80e8 commit a06a1c7

9 files changed

Lines changed: 97 additions & 13 deletions

File tree

ROADMAP.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,12 @@ All 313 `@object-ui/fields` tests pass.
14951495
- [x] Header highlight area with key fields
14961496
- [x] Console `RecordDetailView` integration: `autoTabs`, `autoDiscoverRelated`, `highlightFields`, `sectionGroups` wired into `detailSchema` for end-to-end availability
14971497
- [x] Console reverse-reference discovery: child objects (e.g., `order_item``order`) auto-discovered and rendered with filtered data
1498+
- [x] `useSafeFieldLabel` wired into `DetailSection`, `RelatedList`, `HeaderHighlight` for convention-based field label i18n (#968, #883, #942)
1499+
- [x] `objectName` threaded through `DetailView``SectionGroup``DetailSection` / `HeaderHighlight` / `RelatedList`
1500+
- [x] RelatedList sortable headers fixed to use `effectiveColumns` (auto-generated from schema) instead of raw `columns` prop
1501+
- [x] Added missing `detail.*` i18n keys (`activity`, `editRow`, `deleteRow`, `previousPage`, `nextPage`, etc.) to en.ts and zh.ts
1502+
- [x] RelatedList auto-generated columns use `getCellRenderer` for type-aware cell rendering (date, currency, select/badge, lookup, boolean, etc.)
1503+
- [x] Console `RecordDetailView` fallback section title uses `t('detail.details')` instead of hardcoded English
14981504

14991505
---
15001506

apps/console/src/components/RecordDetailView.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
1212
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
1313
import { PresenceAvatars, type PresenceUser } from '@object-ui/collaboration';
1414
import { useAuth } from '@object-ui/auth';
15-
import { ActionProvider } from '@object-ui/react';
15+
import { ActionProvider, useObjectTranslation } from '@object-ui/react';
1616
import { toast } from 'sonner';
1717
import { Database, Users } from 'lucide-react';
1818
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
@@ -38,6 +38,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
3838
const { showDebug } = useMetadataInspector();
3939
const { user } = useAuth();
4040
const navigate = useNavigate();
41+
const { t } = useObjectTranslation();
4142
const [isLoading, setIsLoading] = useState(true);
4243
const [feedItems, setFeedItems] = useState<FeedItem[]>([]);
4344
const [recordViewers, setRecordViewers] = useState<PresenceUser[]>([]);
@@ -368,7 +369,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
368369
}))
369370
: [
370371
{
371-
title: 'Details',
372+
title: t('detail.details', { defaultValue: 'Details' }),
372373
fields: Object.keys(objectDef.fields || {}).map(key => {
373374
const fieldDef = objectDef.fields[key];
374375
const refTarget = fieldDef.reference_to || fieldDef.reference;

packages/i18n/src/locales/en.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ const en = {
180180
viewAll: 'View All',
181181
new: 'New',
182182
emptyValue: '—',
183+
activity: 'Activity',
184+
editRow: 'Edit',
185+
deleteRow: 'Delete',
186+
deleteRowConfirmation: 'Are you sure you want to delete this record?',
187+
actions: 'Actions',
188+
previousPage: 'Previous',
189+
nextPage: 'Next',
190+
pageOf: 'Page {{current}} of {{total}}',
191+
sortBy: 'Sort by',
192+
filterPlaceholder: 'Filter...',
193+
highlightFields: 'Key Fields',
183194
},
184195
chart: {
185196
noData: 'No chart data available',

packages/i18n/src/locales/zh.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ const zh = {
180180
viewAll: '查看全部',
181181
new: '新建',
182182
emptyValue: '—',
183+
activity: '活动',
184+
editRow: '编辑',
185+
deleteRow: '删除',
186+
deleteRowConfirmation: '确定要删除此记录吗?',
187+
actions: '操作',
188+
previousPage: '上一页',
189+
nextPage: '下一页',
190+
pageOf: '第 {{current}} 页,共 {{total}} 页',
191+
sortBy: '排序',
192+
filterPlaceholder: '筛选...',
193+
highlightFields: '关键字段',
183194
},
184195
chart: {
185196
noData: '暂无图表数据',

packages/plugin-detail/src/DetailSection.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ import { getCellRenderer } from '@object-ui/fields';
2929
import type { DetailViewSection as DetailViewSectionType, DetailViewField, FieldMetadata } from '@object-ui/types';
3030
import { applyDetailAutoLayout } from './autoLayout';
3131
import { useDetailTranslation } from './useDetailTranslation';
32+
import { useSafeFieldLabel } from '@object-ui/react';
3233

3334
export interface DetailSectionProps {
3435
section: DetailViewSectionType;
3536
data?: any;
3637
className?: string;
3738
/** Object schema from DataSource for field type enrichment */
3839
objectSchema?: any;
40+
/** Object name for i18n field label resolution */
41+
objectName?: string;
3942
/** Whether inline editing is active */
4043
isEditing?: boolean;
4144
/** Callback when a field value changes during inline editing */
@@ -47,12 +50,14 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
4750
data,
4851
className,
4952
objectSchema,
53+
objectName,
5054
isEditing = false,
5155
onFieldChange,
5256
}) => {
5357
const [isCollapsed, setIsCollapsed] = React.useState(section.defaultCollapsed ?? false);
5458
const [copiedField, setCopiedField] = React.useState<string | null>(null);
5559
const { t } = useDetailTranslation();
60+
const { fieldLabel } = useSafeFieldLabel();
5661

5762
const handleCopyField = React.useCallback((fieldName: string, value: any) => {
5863
const textValue = value !== null && value !== undefined ? String(value) : '';
@@ -111,7 +116,7 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
111116
return (
112117
<div key={field.name} className={cn("space-y-1.5 group", spanClass)}>
113118
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
114-
{field.label || field.name}
119+
{fieldLabel(objectName || '', field.name, field.label || field.name)}
115120
</div>
116121
{isEditing && !field.readonly ? (
117122
<div className="min-h-[44px] sm:min-h-0">

packages/plugin-detail/src/DetailView.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
590590

591591
{/* Header Highlight Area */}
592592
{schema.highlightFields && schema.highlightFields.length > 0 && (
593-
<HeaderHighlight fields={schema.highlightFields} data={data} />
593+
<HeaderHighlight fields={schema.highlightFields} data={data} objectName={schema.objectName} />
594594
)}
595595

596596
{/* Auto Tabs mode: wrap sections, related, activity into tabs */}
@@ -638,6 +638,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
638638
group={group}
639639
data={{ ...data, ...editedValues }}
640640
objectSchema={objectSchema}
641+
objectName={schema.objectName}
641642
isEditing={isInlineEditing}
642643
onFieldChange={handleInlineFieldChange}
643644
/>
@@ -650,6 +651,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
650651
section={section}
651652
data={{ ...data, ...editedValues }}
652653
objectSchema={objectSchema}
654+
objectName={schema.objectName}
653655
isEditing={isInlineEditing}
654656
onFieldChange={handleInlineFieldChange}
655657
/>
@@ -663,6 +665,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
663665
}}
664666
data={{ ...data, ...editedValues }}
665667
objectSchema={objectSchema}
668+
objectName={schema.objectName}
666669
isEditing={isInlineEditing}
667670
onFieldChange={handleInlineFieldChange}
668671
/>
@@ -690,6 +693,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
690693
data={related.data}
691694
columns={related.columns as any}
692695
dataSource={dataSource}
696+
objectName={related.api}
693697
/>
694698
))}
695699
</div>
@@ -714,6 +718,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
714718
group={group}
715719
data={{ ...data, ...editedValues }}
716720
objectSchema={objectSchema}
721+
objectName={schema.objectName}
717722
isEditing={isInlineEditing}
718723
onFieldChange={handleInlineFieldChange}
719724
/>
@@ -730,6 +735,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
730735
section={section}
731736
data={{ ...data, ...editedValues }}
732737
objectSchema={objectSchema}
738+
objectName={schema.objectName}
733739
isEditing={isInlineEditing}
734740
onFieldChange={handleInlineFieldChange}
735741
/>
@@ -746,6 +752,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
746752
}}
747753
data={{ ...data, ...editedValues }}
748754
objectSchema={objectSchema}
755+
objectName={schema.objectName}
749756
isEditing={isInlineEditing}
750757
onFieldChange={handleInlineFieldChange}
751758
/>
@@ -769,6 +776,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
769776
data={related.data}
770777
columns={related.columns as any}
771778
dataSource={dataSource}
779+
objectName={related.api}
772780
/>
773781
))}
774782
</div>

packages/plugin-detail/src/HeaderHighlight.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,23 @@
99
import * as React from 'react';
1010
import { cn, Card, CardContent } from '@object-ui/components';
1111
import type { HighlightField } from '@object-ui/types';
12+
import { useSafeFieldLabel } from '@object-ui/react';
1213

1314
export interface HeaderHighlightProps {
1415
fields: HighlightField[];
1516
data?: any;
1617
className?: string;
18+
/** Object name for i18n field label resolution */
19+
objectName?: string;
1720
}
1821

1922
export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
2023
fields,
2124
data,
2225
className,
26+
objectName,
2327
}) => {
28+
const { fieldLabel } = useSafeFieldLabel();
2429
if (!fields.length || !data) return null;
2530

2631
// Filter to only fields with values
@@ -47,7 +52,7 @@ export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
4752
<div key={field.name} className="flex flex-col gap-0.5">
4853
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
4954
{field.icon && <span className="mr-1">{field.icon}</span>}
50-
{field.label}
55+
{fieldLabel(objectName || '', field.name, field.label)}
5156
</span>
5257
<span className="text-sm font-semibold truncate">
5358
{String(value)}

packages/plugin-detail/src/RelatedList.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import {
2525
ChevronRight,
2626
ArrowUpDown,
2727
} from 'lucide-react';
28-
import type { DataSource } from '@object-ui/types';
28+
import type { DataSource, FieldMetadata } from '@object-ui/types';
29+
import { getCellRenderer } from '@object-ui/fields';
2930
import { useDetailTranslation } from './useDetailTranslation';
31+
import { useSafeFieldLabel } from '@object-ui/react';
3032

3133
export interface RelatedListProps {
3234
title: string;
@@ -37,6 +39,8 @@ export interface RelatedListProps {
3739
columns?: any[];
3840
className?: string;
3941
dataSource?: DataSource;
42+
/** Object name for i18n field label resolution */
43+
objectName?: string;
4044
/** Callback when "New" button is clicked */
4145
onNew?: () => void;
4246
/** Callback when "View All" button is clicked */
@@ -62,6 +66,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
6266
columns,
6367
className,
6468
dataSource,
69+
objectName,
6570
onNew,
6671
onViewAll,
6772
onRowEdit,
@@ -78,6 +83,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
7883
const [filterText, setFilterText] = React.useState('');
7984
const [objectSchema, setObjectSchema] = React.useState<any>(null);
8085
const { t } = useDetailTranslation();
86+
const { fieldLabel: resolveFieldLabel } = useSafeFieldLabel();
8187

8288
// Sync internal state when data prop changes (e.g., parent fetches async data)
8389
React.useEffect(() => {
@@ -180,13 +186,40 @@ export const RelatedList: React.FC<RelatedListProps> = ({
180186
const effectiveColumns = React.useMemo(() => {
181187
if (columns && columns.length > 0) return columns;
182188
if (!objectSchema?.fields) return [];
189+
const resolvedObjectName = objectName || api || '';
183190
return Object.entries(objectSchema.fields)
184191
.filter(([key]) => !key.startsWith('_'))
185-
.map(([key, def]: [string, any]) => ({
186-
accessorKey: key,
187-
header: def.label || key,
188-
}));
189-
}, [columns, objectSchema]);
192+
.map(([key, def]: [string, any]) => {
193+
const col: any = {
194+
accessorKey: key,
195+
header: resolveFieldLabel(resolvedObjectName, key, def.label || key),
196+
};
197+
// Add type-aware cell renderer for typed fields
198+
if (def.type) {
199+
const CellRenderer = getCellRenderer(def.type);
200+
if (CellRenderer) {
201+
const fieldMeta: FieldMetadata = {
202+
name: key,
203+
label: def.label || key,
204+
type: def.type,
205+
...(def.options && { options: def.options }),
206+
...(def.currency && { currency: def.currency }),
207+
...(def.precision !== undefined && { precision: def.precision }),
208+
...(def.format && { format: def.format }),
209+
...((def.reference_to || def.reference) && { reference_to: def.reference_to || def.reference }),
210+
...(def.reference_field && { reference_field: def.reference_field }),
211+
};
212+
col.cell = (value: any) => {
213+
if (value === null || value === undefined) {
214+
return React.createElement('span', { className: 'text-muted-foreground/50 text-xs italic' }, '—');
215+
}
216+
return React.createElement(CellRenderer, { value, field: fieldMeta });
217+
};
218+
}
219+
}
220+
return col;
221+
});
222+
}, [columns, objectSchema, objectName, api, resolveFieldLabel]);
190223

191224
const viewSchema = React.useMemo(() => {
192225
if (schema) return schema;
@@ -258,9 +291,9 @@ export const RelatedList: React.FC<RelatedListProps> = ({
258291
)}
259292

260293
{/* Sortable column headers */}
261-
{sortable && columns && columns.length > 0 && relatedData.length > 0 && (
294+
{sortable && effectiveColumns && effectiveColumns.length > 0 && relatedData.length > 0 && (
262295
<div className="flex flex-wrap gap-1 mb-3">
263-
{columns.map((col: any) => {
296+
{effectiveColumns.map((col: any) => {
264297
const field = col.accessorKey || col.field || col.name;
265298
if (!field) return null;
266299
const label = col.header || col.label || field;

packages/plugin-detail/src/SectionGroup.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface SectionGroupProps {
2222
data?: any;
2323
className?: string;
2424
objectSchema?: any;
25+
/** Object name for i18n field label resolution */
26+
objectName?: string;
2527
isEditing?: boolean;
2628
onFieldChange?: (field: string, value: any) => void;
2729
}
@@ -31,6 +33,7 @@ export const SectionGroup: React.FC<SectionGroupProps> = ({
3133
data,
3234
className,
3335
objectSchema,
36+
objectName,
3437
isEditing = false,
3538
onFieldChange,
3639
}) => {
@@ -45,6 +48,7 @@ export const SectionGroup: React.FC<SectionGroupProps> = ({
4548
section={section}
4649
data={data}
4750
objectSchema={objectSchema}
51+
objectName={objectName}
4852
isEditing={isEditing}
4953
onFieldChange={onFieldChange}
5054
/>

0 commit comments

Comments
 (0)