This commit is contained in:
Stephen Zhou 2025-12-25 16:43:57 +08:00
parent 3e9c427b06
commit 2559d75aa6
No known key found for this signature in database
23 changed files with 152 additions and 85 deletions

View File

@ -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)
}
})
})

View File

@ -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<string, string> = {
'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}`
},
}),
}))

View File

@ -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<string, string> = {
'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}`
},
}),
}))

View File

@ -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}`
},
}),
}))

View File

@ -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}`
},
}),
}))

View File

@ -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'

View File

@ -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}`
},
}),
}))

View File

@ -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<string, string> = {
'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}`
},
}),
}))

View File

@ -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<string, string> = {
'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}`
},
}),
}))

View File

@ -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<string, string> = {
'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}`
},
}),
}))

View File

@ -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}`
},
}),
}
})

View File

@ -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}`
},
}),
}
})

View File

@ -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 }) => <span>{i18nKey}</span>,

View File

@ -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

View File

@ -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}`
},
}),
}
})

View File

@ -18,7 +18,8 @@ vi.mock('react-i18next', () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (options?.returnObjects)
return featuresTranslations[key] || []
return key
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,

View File

@ -8,7 +8,8 @@ vi.mock('react-i18next', () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (options?.returnObjects)
return ['Feature A', 'Feature B']
return key
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,

View File

@ -7,10 +7,11 @@ import PreviewDocumentPicker from './preview-document-picker'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, params?: Record<string, unknown>) => {
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}`
},
}),
}))

View File

@ -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}`
},
}),
}))

View File

@ -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}`
},
}),
}))

View File

@ -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}`
},
}),
}))

View File

@ -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()

View File

@ -90,12 +90,17 @@ vi.mock('react-i18next', async () => {
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
return {
...actual,
useTranslation: () => ({
useTranslation: (defaultNs?: string) => ({
t: (key: string, options?: Record<string, unknown>) => {
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: {