diff --git a/web/__tests__/i18n-upload-features.test.ts b/web/__tests__/i18n-upload-features.test.ts index b861728f6d..3d0f1d30dd 100644 --- a/web/__tests__/i18n-upload-features.test.ts +++ b/web/__tests__/i18n-upload-features.test.ts @@ -24,14 +24,14 @@ const loadTranslationContent = (locale: string): string => { return fs.readFileSync(filePath, 'utf-8') } -// Helper function to check if upload features exist +// Helper function to check if upload features exist (supports flattened JSON) const hasUploadFeatures = (content: string): { [key: string]: boolean } => { return { - fileUpload: /"fileUpload"\s*:\s*\{/.test(content), - imageUpload: /"imageUpload"\s*:\s*\{/.test(content), - documentUpload: /"documentUpload"\s*:\s*\{/.test(content), - audioUpload: /"audioUpload"\s*:\s*\{/.test(content), - featureBar: /"bar"\s*:\s*\{/.test(content), + fileUpload: /"feature\.fileUpload\.title"/.test(content), + imageUpload: /"feature\.imageUpload\.title"/.test(content), + documentUpload: /"feature\.documentUpload\.title"/.test(content), + audioUpload: /"feature\.audioUpload\.title"/.test(content), + featureBar: /"feature\.bar\.empty"/.test(content), } } @@ -76,12 +76,9 @@ describe('Upload Features i18n Translations - Issue #23062', () => { previouslyMissingLocales.forEach((locale) => { const content = loadTranslationContent(locale) - // Verify audioUpload exists - expect(/"audioUpload"\s*:\s*\{/.test(content)).toBe(true) - - // Verify it has title and description - expect(/"audioUpload"[^}]*"title"\s*:/.test(content)).toBe(true) - expect(/"audioUpload"[^}]*"description"\s*:/.test(content)).toBe(true) + // Verify audioUpload exists with title and description (flattened JSON format) + expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true) + expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true) console.log(`✅ ${locale} - Issue #23062 resolved: audioUpload feature present`) }) @@ -91,28 +88,28 @@ describe('Upload Features i18n Translations - Issue #23062', () => { supportedLocales.forEach((locale) => { const content = loadTranslationContent(locale) - // Check fileUpload has required properties - if (/"fileUpload"\s*:\s*\{/.test(content)) { - expect(/"fileUpload"[^}]*"title"\s*:/.test(content)).toBe(true) - expect(/"fileUpload"[^}]*"description"\s*:/.test(content)).toBe(true) + // Check fileUpload has required properties (flattened JSON format) + if (/"feature\.fileUpload\.title"/.test(content)) { + expect(/"feature\.fileUpload\.title"/.test(content)).toBe(true) + expect(/"feature\.fileUpload\.description"/.test(content)).toBe(true) } // Check imageUpload has required properties - if (/"imageUpload"\s*:\s*\{/.test(content)) { - expect(/"imageUpload"[^}]*"title"\s*:/.test(content)).toBe(true) - expect(/"imageUpload"[^}]*"description"\s*:/.test(content)).toBe(true) + if (/"feature\.imageUpload\.title"/.test(content)) { + expect(/"feature\.imageUpload\.title"/.test(content)).toBe(true) + expect(/"feature\.imageUpload\.description"/.test(content)).toBe(true) } // Check documentUpload has required properties - if (/"documentUpload"\s*:\s*\{/.test(content)) { - expect(/"documentUpload"[^}]*"title"\s*:/.test(content)).toBe(true) - expect(/"documentUpload"[^}]*"description"\s*:/.test(content)).toBe(true) + if (/"feature\.documentUpload\.title"/.test(content)) { + expect(/"feature\.documentUpload\.title"/.test(content)).toBe(true) + expect(/"feature\.documentUpload\.description"/.test(content)).toBe(true) } // Check audioUpload has required properties - if (/"audioUpload"\s*:\s*\{/.test(content)) { - expect(/"audioUpload"[^}]*"title"\s*:/.test(content)).toBe(true) - expect(/"audioUpload"[^}]*"description"\s*:/.test(content)).toBe(true) + if (/"feature\.audioUpload\.title"/.test(content)) { + expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true) + expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true) } }) }) diff --git a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.spec.tsx b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.spec.tsx index 5dbfd664f8..790c013b05 100644 --- a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.spec.tsx +++ b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.spec.tsx @@ -4,13 +4,16 @@ import ClearAllAnnotationsConfirmModal from './index' vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => { + t: (key: string, options?: { ns?: string }) => { const translations: Record = { - 'appAnnotation.table.header.clearAllConfirm': 'Clear all annotations?', - 'common.operation.confirm': 'Confirm', - 'common.operation.cancel': 'Cancel', + 'table.header.clearAllConfirm': 'Clear all annotations?', + 'operation.confirm': 'Confirm', + 'operation.cancel': 'Cancel', } - return translations[key] || key + if (translations[key]) + return translations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.spec.tsx b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.spec.tsx index db3bb63c43..c51ee4487f 100644 --- a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.spec.tsx +++ b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.spec.tsx @@ -4,13 +4,16 @@ import RemoveAnnotationConfirmModal from './index' vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => { + t: (key: string, options?: { ns?: string }) => { const translations: Record = { - 'appDebug.feature.annotation.removeConfirm': 'Remove annotation?', - 'common.operation.confirm': 'Confirm', - 'common.operation.cancel': 'Cancel', + 'feature.annotation.removeConfirm': 'Remove annotation?', + 'operation.confirm': 'Confirm', + 'operation.cancel': 'Cancel', } - return translations[key] || key + if (translations[key]) + return translations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/app/configuration/config/agent-setting-button.spec.tsx b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx index 963a671a23..2f643616be 100644 --- a/web/app/components/app/configuration/config/agent-setting-button.spec.tsx +++ b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx @@ -7,7 +7,10 @@ import AgentSettingButton from './agent-setting-button' vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: (key: string, options?: { ns?: string }) => { + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), })) diff --git a/web/app/components/app/configuration/config/config-audio.spec.tsx b/web/app/components/app/configuration/config/config-audio.spec.tsx index c29e2ac2b4..41219fd1fa 100644 --- a/web/app/components/app/configuration/config/config-audio.spec.tsx +++ b/web/app/components/app/configuration/config/config-audio.spec.tsx @@ -17,7 +17,10 @@ vi.mock('use-context-selector', async (importOriginal) => { vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: (key: string, options?: { ns?: string }) => { + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), })) diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx index 1d67ffb468..3bff63c826 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx @@ -168,7 +168,10 @@ describe('RetrievalChangeTip', () => { }) describe('RetrievalSection', () => { - const t = (key: string) => key + const t = (key: string, options?: { ns?: string }) => { + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + } const rowClass = 'row' const labelClass = 'label' diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx index 596b1a69b6..47fbf9ae32 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx @@ -6,16 +6,17 @@ import TimePicker from './index' vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => { - if (key === 'time.defaultPlaceholder') + t: (key: string, options?: { ns?: string }) => { + if (key === 'defaultPlaceholder') return 'Pick a time...' - if (key === 'time.operation.now') + if (key === 'operation.now') return 'Now' - if (key === 'time.operation.ok') + if (key === 'operation.ok') return 'OK' - if (key === 'common.operation.clear') + if (key === 'operation.clear') return 'Clear' - return key + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/base/inline-delete-confirm/index.spec.tsx b/web/app/components/base/inline-delete-confirm/index.spec.tsx index 8b360929b5..0de8c0844f 100644 --- a/web/app/components/base/inline-delete-confirm/index.spec.tsx +++ b/web/app/components/base/inline-delete-confirm/index.spec.tsx @@ -5,14 +5,20 @@ import InlineDeleteConfirm from './index' // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => { + t: (key: string, defaultValueOrOptions?: string | { ns?: string }) => { const translations: Record = { - 'common.operation.deleteConfirmTitle': 'Delete?', - 'common.operation.yes': 'Yes', - 'common.operation.no': 'No', - 'common.operation.confirmAction': 'Please confirm your action.', + 'operation.deleteConfirmTitle': 'Delete?', + 'operation.yes': 'Yes', + 'operation.no': 'No', + 'operation.confirmAction': 'Please confirm your action.', } - return translations[key] || key + if (translations[key]) + return translations[key] + // Handle case where second arg is default value string + if (typeof defaultValueOrOptions === 'string') + return defaultValueOrOptions + const prefix = defaultValueOrOptions?.ns ? `${defaultValueOrOptions.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/base/input-with-copy/index.spec.tsx b/web/app/components/base/input-with-copy/index.spec.tsx index a5fde4ea44..5a4ca7c97e 100644 --- a/web/app/components/base/input-with-copy/index.spec.tsx +++ b/web/app/components/base/input-with-copy/index.spec.tsx @@ -13,14 +13,17 @@ vi.mock('copy-to-clipboard', () => ({ // Mock the i18n hook vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => { + t: (key: string, options?: { ns?: string }) => { const translations: Record = { - 'common.operation.copy': 'Copy', - 'common.operation.copied': 'Copied', - 'appOverview.overview.appInfo.embedded.copy': 'Copy', - 'appOverview.overview.appInfo.embedded.copied': 'Copied', + 'operation.copy': 'Copy', + 'operation.copied': 'Copied', + 'overview.appInfo.embedded.copy': 'Copy', + 'overview.appInfo.embedded.copied': 'Copied', } - return translations[key] || key + if (translations[key]) + return translations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/base/input/index.spec.tsx b/web/app/components/base/input/index.spec.tsx index 226217a146..a0de3c4ca4 100644 --- a/web/app/components/base/input/index.spec.tsx +++ b/web/app/components/base/input/index.spec.tsx @@ -5,12 +5,15 @@ import Input, { inputVariants } from './index' // Mock the i18n hook vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => { + t: (key: string, options?: { ns?: string }) => { const translations: Record = { - 'common.operation.search': 'Search', - 'common.placeholder.input': 'Please input', + 'operation.search': 'Search', + 'placeholder.input': 'Please input', } - return translations[key] || '' + if (translations[key]) + return translations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/billing/pricing/footer.spec.tsx b/web/app/components/billing/pricing/footer.spec.tsx index f8e7965f5e..0bbc38224e 100644 --- a/web/app/components/billing/pricing/footer.spec.tsx +++ b/web/app/components/billing/pricing/footer.spec.tsx @@ -18,7 +18,12 @@ vi.mock('react-i18next', async (importOriginal) => { return { ...actual, useTranslation: () => ({ - t: (key: string) => mockTranslations[key] ?? key, + t: (key: string, options?: { ns?: string }) => { + if (mockTranslations[key]) + return mockTranslations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), } }) diff --git a/web/app/components/billing/pricing/header.spec.tsx b/web/app/components/billing/pricing/header.spec.tsx index 0395e5dd48..6c32af23b2 100644 --- a/web/app/components/billing/pricing/header.spec.tsx +++ b/web/app/components/billing/pricing/header.spec.tsx @@ -9,7 +9,12 @@ vi.mock('react-i18next', async (importOriginal) => { return { ...actual, useTranslation: () => ({ - t: (key: string) => mockTranslations[key] ?? key, + t: (key: string, options?: { ns?: string }) => { + if (mockTranslations[key]) + return mockTranslations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), } }) diff --git a/web/app/components/billing/pricing/index.spec.tsx b/web/app/components/billing/pricing/index.spec.tsx index 141c2d9c96..89f39bd75c 100644 --- a/web/app/components/billing/pricing/index.spec.tsx +++ b/web/app/components/billing/pricing/index.spec.tsx @@ -41,10 +41,13 @@ vi.mock('react-i18next', async (importOriginal) => { return { ...actual, useTranslation: () => ({ - t: (key: string, options?: { returnObjects?: boolean }) => { + t: (key: string, options?: { returnObjects?: boolean, ns?: string }) => { if (options?.returnObjects) return mockTranslations[key] ?? [] - return mockTranslations[key] ?? key + if (mockTranslations[key]) + return mockTranslations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), Trans: ({ i18nKey }: { i18nKey: string }) => {i18nKey}, diff --git a/web/app/components/billing/pricing/plan-switcher/index.spec.tsx b/web/app/components/billing/pricing/plan-switcher/index.spec.tsx index 641d359bfd..6a3af8a589 100644 --- a/web/app/components/billing/pricing/plan-switcher/index.spec.tsx +++ b/web/app/components/billing/pricing/plan-switcher/index.spec.tsx @@ -11,7 +11,12 @@ vi.mock('react-i18next', async (importOriginal) => { return { ...actual, useTranslation: () => ({ - t: (key: string) => mockTranslations[key] ?? key, + t: (key: string, options?: { ns?: string }) => { + if (key in mockTranslations) + return mockTranslations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), } }) @@ -85,8 +90,8 @@ describe('PlanSwitcher', () => { it('should render tabs when translation strings are empty', () => { // Arrange mockTranslations = { - 'billing.plansCommon.cloud': '', - 'billing.plansCommon.self': '', + 'plansCommon.cloud': '', + 'plansCommon.self': '', } // Act diff --git a/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx b/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx index 0b4c00603c..50634bec5e 100644 --- a/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx +++ b/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx @@ -9,7 +9,12 @@ vi.mock('react-i18next', async (importOriginal) => { return { ...actual, useTranslation: () => ({ - t: (key: string) => mockTranslations[key] ?? key, + t: (key: string, options?: { ns?: string }) => { + if (mockTranslations[key]) + return mockTranslations[key] + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), } }) diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.spec.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.spec.tsx index 3a687de5c1..610d9264f0 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.spec.tsx @@ -18,7 +18,8 @@ vi.mock('react-i18next', () => ({ t: (key: string, options?: Record) => { if (options?.returnObjects) return featuresTranslations[key] || [] - return key + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), Trans: ({ i18nKey }: { i18nKey: string }) => {i18nKey}, diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.spec.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.spec.tsx index 97329e47e5..1399c2c390 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.spec.tsx @@ -8,7 +8,8 @@ vi.mock('react-i18next', () => ({ t: (key: string, options?: Record) => { if (options?.returnObjects) return ['Feature A', 'Feature B'] - return key + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), Trans: ({ i18nKey }: { i18nKey: string }) => {i18nKey}, diff --git a/web/app/components/datasets/common/document-picker/preview-document-picker.spec.tsx b/web/app/components/datasets/common/document-picker/preview-document-picker.spec.tsx index 65272b77be..ab46eb4bdc 100644 --- a/web/app/components/datasets/common/document-picker/preview-document-picker.spec.tsx +++ b/web/app/components/datasets/common/document-picker/preview-document-picker.spec.tsx @@ -7,10 +7,11 @@ import PreviewDocumentPicker from './preview-document-picker' vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, params?: Record) => { - if (key === 'dataset.preprocessDocument' && params?.num) + if (key === 'preprocessDocument' && params?.num) return `${params.num} files` - return key + const prefix = params?.ns ? `${params.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/datasets/create/index.spec.tsx b/web/app/components/datasets/create/index.spec.tsx index 66a642db3a..a7a6ab682f 100644 --- a/web/app/components/datasets/create/index.spec.tsx +++ b/web/app/components/datasets/create/index.spec.tsx @@ -21,7 +21,10 @@ const IndexingTypeValues = { // Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages) vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: (key: string, options?: { ns?: string }) => { + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), })) diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx index 9d7a3e7b08..f0f0bb9af6 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/index.spec.tsx @@ -12,7 +12,10 @@ import Processing from './index' // Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages) vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: (key: string, options?: { ns?: string }) => { + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, }), })) diff --git a/web/app/components/datasets/documents/detail/completed/segment-card/index.spec.tsx b/web/app/components/datasets/documents/detail/completed/segment-card/index.spec.tsx index 536b7af338..19b1bdace1 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-card/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-card/index.spec.tsx @@ -9,12 +9,13 @@ import SegmentCard from './index' // Mock react-i18next - external dependency vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string, options?: { count?: number }) => { - if (key === 'datasetDocuments.segment.characters') + t: (key: string, options?: { count?: number, ns?: string }) => { + if (key === 'segment.characters') return options?.count === 1 ? 'character' : 'characters' - if (key === 'datasetDocuments.segment.childChunks') + if (key === 'segment.childChunks') return options?.count === 1 ? 'child chunk' : 'child chunks' - return key + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` }, }), })) diff --git a/web/app/components/explore/create-app-modal/index.spec.tsx b/web/app/components/explore/create-app-modal/index.spec.tsx index 6bc1e1e9a0..979ecc6caa 100644 --- a/web/app/components/explore/create-app-modal/index.spec.tsx +++ b/web/app/components/explore/create-app-modal/index.spec.tsx @@ -17,8 +17,12 @@ vi.mock('react-i18next', () => ({ return override if (options?.returnObjects) return [`${key}-feature-1`, `${key}-feature-2`] - if (options) - return `${key}:${JSON.stringify(options)}` + if (options) { + const { ns, ...rest } = options + const prefix = ns ? `${ns}.` : '' + const suffix = Object.keys(rest).length > 0 ? `:${JSON.stringify(rest)}` : '' + return `${prefix}${key}${suffix}` + } return key }, i18n: { @@ -192,8 +196,8 @@ describe('CreateAppModal', () => { it('should fall back to empty placeholders when translations return empty string', () => { mockTranslationOverrides = { - 'app.newApp.appNamePlaceholder': '', - 'app.newApp.appDescriptionPlaceholder': '', + 'newApp.appNamePlaceholder': '', + 'newApp.appDescriptionPlaceholder': '', } setup() diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index e4ea9ed31c..551a22475b 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -90,12 +90,17 @@ vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next') return { ...actual, - useTranslation: () => ({ + useTranslation: (defaultNs?: string) => ({ t: (key: string, options?: Record) => { if (options?.returnObjects) return [`${key}-feature-1`, `${key}-feature-2`] - if (options) - return `${key}:${JSON.stringify(options)}` + const ns = options?.ns ?? defaultNs + if (options || ns) { + const { ns: _ns, ...rest } = options ?? {} + const prefix = ns ? `${ns}.` : '' + const suffix = Object.keys(rest).length > 0 ? `:${JSON.stringify(rest)}` : '' + return `${prefix}${key}${suffix}` + } return key }, i18n: {