Skip to content

Commit ef4453f

Browse files
Copilothotlong
andcommitted
refactor: extract shared createSafeTranslationHook to reduce duplication
- Create useDetailTranslation.ts with shared DETAIL_DEFAULT_TRANSLATIONS and createSafeTranslationHook utility - DetailView, DetailSection, and RelatedList all use the shared hook - Remove duplicate translation constants and hook implementations - Export useDetailTranslation, DETAIL_DEFAULT_TRANSLATIONS, and createSafeTranslationHook from plugin-detail index Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent d84d066 commit ef4453f

File tree

5 files changed

+112
-142
lines changed

5 files changed

+112
-142
lines changed

packages/plugin-detail/src/DetailSection.tsx

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,11 @@ import {
2424
TooltipTrigger,
2525
} from '@object-ui/components';
2626
import { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react';
27-
import { SchemaRenderer, useObjectTranslation } from '@object-ui/react';
27+
import { SchemaRenderer } from '@object-ui/react';
2828
import { getCellRenderer } from '@object-ui/fields';
2929
import type { DetailViewSection as DetailViewSectionType, DetailViewField, FieldMetadata } from '@object-ui/types';
3030
import { applyDetailAutoLayout } from './autoLayout';
31-
32-
const SECTION_TRANSLATIONS: Record<string, string> = {
33-
'detail.copyToClipboard': 'Copy to clipboard',
34-
'detail.copied': 'Copied!',
35-
};
36-
37-
function useSectionTranslation() {
38-
try {
39-
const result = useObjectTranslation();
40-
const testValue = result.t('detail.copyToClipboard');
41-
if (testValue === 'detail.copyToClipboard') {
42-
return { t: (key: string) => SECTION_TRANSLATIONS[key] || key };
43-
}
44-
return { t: result.t };
45-
} catch {
46-
return { t: (key: string) => SECTION_TRANSLATIONS[key] || key };
47-
}
48-
}
31+
import { useDetailTranslation } from './useDetailTranslation';
4932

5033
export interface DetailSectionProps {
5134
section: DetailViewSectionType;
@@ -69,7 +52,7 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
6952
}) => {
7053
const [isCollapsed, setIsCollapsed] = React.useState(section.defaultCollapsed ?? false);
7154
const [copiedField, setCopiedField] = React.useState<string | null>(null);
72-
const { t } = useSectionTranslation();
55+
const { t } = useDetailTranslation();
7356

7457
const handleCopyField = React.useCallback((fieldName: string, value: any) => {
7558
const textValue = value !== null && value !== undefined ? String(value) : '';

packages/plugin-detail/src/DetailView.tsx

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -42,86 +42,10 @@ import { DetailTabs } from './DetailTabs';
4242
import { RelatedList } from './RelatedList';
4343
import { RecordComments } from './RecordComments';
4444
import { ActivityTimeline } from './ActivityTimeline';
45-
import { SchemaRenderer, useObjectTranslation } from '@object-ui/react';
45+
import { SchemaRenderer } from '@object-ui/react';
4646
import { buildExpandFields } from '@object-ui/core';
4747
import type { DetailViewSchema, DataSource } from '@object-ui/types';
48-
49-
/**
50-
* Default English translations for the detail view.
51-
* Used as fallback when no I18nProvider is available.
52-
*/
53-
const DETAIL_DEFAULT_TRANSLATIONS: Record<string, string> = {
54-
'detail.back': 'Back',
55-
'detail.edit': 'Edit',
56-
'detail.editInline': 'Edit inline',
57-
'detail.save': 'Save',
58-
'detail.saveChanges': 'Save changes',
59-
'detail.editFieldsInline': 'Edit fields inline',
60-
'detail.share': 'Share',
61-
'detail.duplicate': 'Duplicate',
62-
'detail.export': 'Export',
63-
'detail.viewHistory': 'View history',
64-
'detail.delete': 'Delete',
65-
'detail.moreActions': 'More actions',
66-
'detail.addToFavorites': 'Add to favorites',
67-
'detail.removeFromFavorites': 'Remove from favorites',
68-
'detail.previousRecord': 'Previous record',
69-
'detail.nextRecord': 'Next record',
70-
'detail.recordOf': '{{current}} of {{total}}',
71-
'detail.recordNotFound': 'Record not found',
72-
'detail.recordNotFoundDescription': 'The record you are looking for does not exist or may have been deleted.',
73-
'detail.goBack': 'Go back',
74-
'detail.details': 'Details',
75-
'detail.related': 'Related',
76-
'detail.relatedRecords': '{{count}} records',
77-
'detail.relatedRecordOne': '{{count}} record',
78-
'detail.noRelatedRecords': 'No related records found',
79-
'detail.loading': 'Loading...',
80-
'detail.copyToClipboard': 'Copy to clipboard',
81-
'detail.copied': 'Copied!',
82-
'detail.deleteConfirmation': 'Are you sure you want to delete this record?',
83-
'detail.editRecord': 'Edit record',
84-
'detail.viewAll': 'View All',
85-
'detail.new': 'New',
86-
'detail.emptyValue': '—',
87-
};
88-
89-
/**
90-
* Safe wrapper for useObjectTranslation that falls back to English defaults
91-
* when I18nProvider is not available (e.g., standalone usage).
92-
*/
93-
function useDetailTranslation() {
94-
try {
95-
const result = useObjectTranslation();
96-
const testValue = result.t('detail.back');
97-
if (testValue === 'detail.back') {
98-
return {
99-
t: (key: string, options?: Record<string, unknown>) => {
100-
let value = DETAIL_DEFAULT_TRANSLATIONS[key] || key;
101-
if (options) {
102-
for (const [k, v] of Object.entries(options)) {
103-
value = value.replace(`{{${k}}}`, String(v));
104-
}
105-
}
106-
return value;
107-
},
108-
};
109-
}
110-
return { t: result.t };
111-
} catch {
112-
return {
113-
t: (key: string, options?: Record<string, unknown>) => {
114-
let value = DETAIL_DEFAULT_TRANSLATIONS[key] || key;
115-
if (options) {
116-
for (const [k, v] of Object.entries(options)) {
117-
value = value.replace(`{{${k}}}`, String(v));
118-
}
119-
}
120-
return value;
121-
},
122-
};
123-
}
124-
}
48+
import { useDetailTranslation } from './useDetailTranslation';
12549

12650
export interface DetailViewProps {
12751
schema: DetailViewSchema;

packages/plugin-detail/src/RelatedList.tsx

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,10 @@
88

99
import * as React from 'react';
1010
import { Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui/components';
11-
import { SchemaRenderer, useObjectTranslation } from '@object-ui/react';
11+
import { SchemaRenderer } from '@object-ui/react';
1212
import { Plus, ExternalLink } from 'lucide-react';
1313
import type { DataSource } from '@object-ui/types';
14-
15-
const RELATED_TRANSLATIONS: Record<string, string> = {
16-
'detail.relatedRecords': '{{count}} records',
17-
'detail.relatedRecordOne': '{{count}} record',
18-
'detail.noRelatedRecords': 'No related records found',
19-
'detail.loading': 'Loading...',
20-
'detail.viewAll': 'View All',
21-
'detail.new': 'New',
22-
};
23-
24-
function useRelatedTranslation() {
25-
try {
26-
const result = useObjectTranslation();
27-
const testValue = result.t('detail.loading');
28-
if (testValue === 'detail.loading') {
29-
return {
30-
t: (key: string, options?: Record<string, unknown>) => {
31-
let value = RELATED_TRANSLATIONS[key] || key;
32-
if (options) {
33-
for (const [k, v] of Object.entries(options)) {
34-
value = value.replace(`{{${k}}}`, String(v));
35-
}
36-
}
37-
return value;
38-
},
39-
};
40-
}
41-
return { t: result.t };
42-
} catch {
43-
return {
44-
t: (key: string, options?: Record<string, unknown>) => {
45-
let value = RELATED_TRANSLATIONS[key] || key;
46-
if (options) {
47-
for (const [k, v] of Object.entries(options)) {
48-
value = value.replace(`{{${k}}}`, String(v));
49-
}
50-
}
51-
return value;
52-
},
53-
};
54-
}
55-
}
14+
import { useDetailTranslation } from './useDetailTranslation';
5615

5716
export interface RelatedListProps {
5817
title: string;
@@ -83,7 +42,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
8342
}) => {
8443
const [relatedData, setRelatedData] = React.useState(data);
8544
const [loading, setLoading] = React.useState(false);
86-
const { t } = useRelatedTranslation();
45+
const { t } = useDetailTranslation();
8746

8847
React.useEffect(() => {
8948
if (api && !data.length) {

packages/plugin-detail/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { DetailViewSchema } from '@object-ui/types';
1515

1616
export { DetailView, DetailSection, DetailTabs, RelatedList };
1717
export { inferDetailColumns, isWideFieldType, applyAutoSpan, applyDetailAutoLayout } from './autoLayout';
18+
export { useDetailTranslation, DETAIL_DEFAULT_TRANSLATIONS, createSafeTranslationHook } from './useDetailTranslation';
1819
export { RecordComments } from './RecordComments';
1920
export { ActivityTimeline } from './ActivityTimeline';
2021
export { InlineCreateRelated } from './InlineCreateRelated';
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { useObjectTranslation } from '@object-ui/react';
10+
11+
/**
12+
* Create a safe translation hook with fallback to defaults.
13+
* Follows the same pattern as useGridTranslation / useListViewTranslation.
14+
*
15+
* @param defaults - Fallback English translations keyed by i18n key
16+
* @param testKey - A key to test if i18n is properly configured
17+
*/
18+
export function createSafeTranslationHook(
19+
defaults: Record<string, string>,
20+
testKey: string,
21+
) {
22+
return function useSafeTranslation() {
23+
try {
24+
const result = useObjectTranslation();
25+
const testValue = result.t(testKey);
26+
if (testValue === testKey) {
27+
return {
28+
t: (key: string, options?: Record<string, unknown>) => {
29+
let value = defaults[key] || key;
30+
if (options) {
31+
for (const [k, v] of Object.entries(options)) {
32+
value = value.replace(`{{${k}}}`, String(v));
33+
}
34+
}
35+
return value;
36+
},
37+
};
38+
}
39+
return { t: result.t };
40+
} catch {
41+
return {
42+
t: (key: string, options?: Record<string, unknown>) => {
43+
let value = defaults[key] || key;
44+
if (options) {
45+
for (const [k, v] of Object.entries(options)) {
46+
value = value.replace(`{{${k}}}`, String(v));
47+
}
48+
}
49+
return value;
50+
},
51+
};
52+
}
53+
};
54+
}
55+
56+
/**
57+
* Default English translations for detail view components.
58+
* Used as fallback when no I18nProvider is available.
59+
*/
60+
export const DETAIL_DEFAULT_TRANSLATIONS: Record<string, string> = {
61+
'detail.back': 'Back',
62+
'detail.edit': 'Edit',
63+
'detail.editInline': 'Edit inline',
64+
'detail.save': 'Save',
65+
'detail.saveChanges': 'Save changes',
66+
'detail.editFieldsInline': 'Edit fields inline',
67+
'detail.share': 'Share',
68+
'detail.duplicate': 'Duplicate',
69+
'detail.export': 'Export',
70+
'detail.viewHistory': 'View history',
71+
'detail.delete': 'Delete',
72+
'detail.moreActions': 'More actions',
73+
'detail.addToFavorites': 'Add to favorites',
74+
'detail.removeFromFavorites': 'Remove from favorites',
75+
'detail.previousRecord': 'Previous record',
76+
'detail.nextRecord': 'Next record',
77+
'detail.recordOf': '{{current}} of {{total}}',
78+
'detail.recordNotFound': 'Record not found',
79+
'detail.recordNotFoundDescription': 'The record you are looking for does not exist or may have been deleted.',
80+
'detail.goBack': 'Go back',
81+
'detail.details': 'Details',
82+
'detail.related': 'Related',
83+
'detail.relatedRecords': '{{count}} records',
84+
'detail.relatedRecordOne': '{{count}} record',
85+
'detail.noRelatedRecords': 'No related records found',
86+
'detail.loading': 'Loading...',
87+
'detail.copyToClipboard': 'Copy to clipboard',
88+
'detail.copied': 'Copied!',
89+
'detail.deleteConfirmation': 'Are you sure you want to delete this record?',
90+
'detail.editRecord': 'Edit record',
91+
'detail.viewAll': 'View All',
92+
'detail.new': 'New',
93+
'detail.emptyValue': '—',
94+
};
95+
96+
/**
97+
* Translation hook for detail view components.
98+
* Falls back to DETAIL_DEFAULT_TRANSLATIONS when no I18nProvider is available.
99+
*/
100+
export const useDetailTranslation = createSafeTranslationHook(
101+
DETAIL_DEFAULT_TRANSLATIONS,
102+
'detail.back',
103+
);

0 commit comments

Comments
 (0)