feat: integrate i18n support into migration process

- Added internationalization support to the MigrationApp component, enabling dynamic language changes.
- Updated button labels and informational texts to use translation keys for better localization.
- Introduced a language selector to allow users to switch between languages during the migration process.
- Ensured that the migration process waits for i18n initialization before rendering the main application.
This commit is contained in:
fullex 2025-11-20 22:08:22 +08:00
parent db10bdd539
commit 1685590a07
5 changed files with 257 additions and 45 deletions

View File

@ -1,9 +1,10 @@
import { Button } from '@cherrystudio/ui'
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@cherrystudio/ui'
import { AppLogo } from '@renderer/config/env'
import { loggerService } from '@renderer/services/LoggerService'
import { Progress, Space, Steps } from 'antd'
import { AlertTriangle, CheckCircle, CheckCircle2, Database, Loader2, Rocket } from 'lucide-react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { MigratorProgressList } from './components'
@ -14,10 +15,15 @@ import { MigrationIpcChannels } from './types'
const logger = loggerService.withContext('MigrationApp')
const MigrationApp: React.FC = () => {
const { t, i18n } = useTranslation()
const { progress, lastError, confirmComplete } = useMigrationProgress()
const actions = useMigrationActions()
const [isLoading, setIsLoading] = useState(false)
const handleLanguageChange = (lang: string) => {
i18n.changeLanguage(lang)
}
const handleStartMigration = async () => {
setIsLoading(true)
try {
@ -118,19 +124,19 @@ const MigrationApp: React.FC = () => {
case 'introduction':
return (
<>
<Button onClick={actions.cancel}></Button>
<Button onClick={actions.cancel}>{t('migration.buttons.cancel')}</Button>
<Spacer />
<Button onClick={actions.proceedToBackup}></Button>
<Button onClick={actions.proceedToBackup}>{t('migration.buttons.next')}</Button>
</>
)
case 'backup_required':
return (
<>
<Button onClick={actions.cancel}></Button>
<Button onClick={actions.cancel}>{t('migration.buttons.cancel')}</Button>
<Spacer />
<Space>
<Button onClick={actions.showBackupDialog}></Button>
<Button onClick={actions.confirmBackup}></Button>
<Button onClick={actions.showBackupDialog}>{t('migration.buttons.create_backup')}</Button>
<Button onClick={actions.confirmBackup}>{t('migration.buttons.confirm_backup')}</Button>
</Space>
</>
)
@ -139,17 +145,17 @@ const MigrationApp: React.FC = () => {
<ButtonRow>
<div></div>
<Button disabled loading>
...
{t('migration.buttons.backing_up')}
</Button>
</ButtonRow>
)
case 'backup_confirmed':
return (
<ButtonRow>
<Button onClick={actions.cancel}></Button>
<Button onClick={actions.cancel}>{t('migration.buttons.cancel')}</Button>
<Space>
<Button onClick={handleStartMigration} loading={isLoading}>
{t('migration.buttons.start_migration')}
</Button>
</Space>
</ButtonRow>
@ -158,29 +164,29 @@ const MigrationApp: React.FC = () => {
return (
<ButtonRow>
<div></div>
<Button disabled>...</Button>
<Button disabled>{t('migration.buttons.migrating')}</Button>
</ButtonRow>
)
case 'migration_completed':
return (
<ButtonRow>
<div></div>
<Button onClick={confirmComplete}></Button>
<Button onClick={confirmComplete}>{t('migration.buttons.confirm')}</Button>
</ButtonRow>
)
case 'completed':
return (
<ButtonRow>
<div></div>
<Button onClick={actions.restart}></Button>
<Button onClick={actions.restart}>{t('migration.buttons.restart')}</Button>
</ButtonRow>
)
case 'error':
return (
<ButtonRow>
<Button onClick={actions.cancel}></Button>
<Button onClick={actions.cancel}>{t('migration.buttons.close')}</Button>
<Space>
<Button onClick={actions.retry}></Button>
<Button onClick={actions.retry}>{t('migration.buttons.retry')}</Button>
</Space>
</ButtonRow>
)
@ -193,7 +199,7 @@ const MigrationApp: React.FC = () => {
<Container>
<Header>
<HeaderLogo src={AppLogo} />
<HeaderTitle></HeaderTitle>
<HeaderTitle>{t('migration.title')}</HeaderTitle>
</Header>
<MainContent>
@ -204,9 +210,25 @@ const MigrationApp: React.FC = () => {
current={currentStep}
status={stepStatus}
size="small"
items={[{ title: '介绍' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
items={[
{ title: t('migration.stages.introduction') },
{ title: t('migration.stages.backup') },
{ title: t('migration.stages.migration') },
{ title: t('migration.stages.completed') }
]}
/>
</StepsContainer>
<LanguageSelectorContainer>
<Select value={i18n.language} onValueChange={handleLanguageChange}>
<SelectTrigger size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-CN"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
</SelectContent>
</Select>
</LanguageSelectorContainer>
</LeftSidebar>
<RightContent>
@ -215,46 +237,44 @@ const MigrationApp: React.FC = () => {
{progress.stage === 'introduction' && (
<InfoCard>
<InfoTitle></InfoTitle>
<InfoTitle>{t('migration.introduction.title')}</InfoTitle>
<InfoDescription>
Cherry Studio对数据的存储和使用方式进行了重大重构
{t('migration.introduction.description_1')}
<br />
<br />
使
{t('migration.introduction.description_2')}
<br />
<br />
使
{t('migration.introduction.description_3')}
</InfoDescription>
</InfoCard>
)}
{progress.stage === 'backup_required' && (
<InfoCard variant="warning">
<InfoTitle></InfoTitle>
<InfoDescription>
</InfoDescription>
<InfoTitle>{t('migration.backup_required.title')}</InfoTitle>
<InfoDescription>{t('migration.backup_required.description')}</InfoDescription>
</InfoCard>
)}
{progress.stage === 'backup_progress' && (
<InfoCard variant="warning">
<InfoTitle></InfoTitle>
<InfoDescription></InfoDescription>
<InfoTitle>{t('migration.backup_progress.title')}</InfoTitle>
<InfoDescription>{t('migration.backup_progress.description')}</InfoDescription>
</InfoCard>
)}
{progress.stage === 'backup_confirmed' && (
<InfoCard variant="success">
<InfoTitle></InfoTitle>
<InfoDescription></InfoDescription>
<InfoTitle>{t('migration.backup_confirmed.title')}</InfoTitle>
<InfoDescription>{t('migration.backup_confirmed.description')}</InfoDescription>
</InfoCard>
)}
{progress.stage === 'migration' && (
<div style={{ width: '100%', maxWidth: '600px', margin: '0 auto' }}>
<InfoCard>
<InfoTitle>...</InfoTitle>
<InfoTitle>{t('migration.migration.title')}</InfoTitle>
<InfoDescription>{progress.currentMessage}</InfoDescription>
</InfoCard>
<ProgressContainer>
@ -273,8 +293,8 @@ const MigrationApp: React.FC = () => {
{progress.stage === 'migration_completed' && (
<div style={{ width: '100%', maxWidth: '600px', margin: '0 auto' }}>
<InfoCard variant="success">
<InfoTitle></InfoTitle>
<InfoDescription></InfoDescription>
<InfoTitle>{t('migration.migration_completed.title')}</InfoTitle>
<InfoDescription>{t('migration.migration_completed.description')}</InfoDescription>
</InfoCard>
<ProgressContainer>
<Progress percent={100} strokeColor={getProgressColor()} trailColor="#f0f0f0" />
@ -287,19 +307,20 @@ const MigrationApp: React.FC = () => {
{progress.stage === 'completed' && (
<InfoCard variant="success">
<InfoTitle></InfoTitle>
<InfoDescription>使</InfoDescription>
<InfoTitle>{t('migration.completed.title')}</InfoTitle>
<InfoDescription>{t('migration.completed.description')}</InfoDescription>
</InfoCard>
)}
{progress.stage === 'error' && (
<InfoCard variant="error">
<InfoTitle></InfoTitle>
<InfoTitle>{t('migration.error.title')}</InfoTitle>
<InfoDescription>
使
{t('migration.error.description')}
<br />
<br />
{lastError || progress.error || '发生未知错误'}
{t('migration.error.error_prefix')}
{lastError || progress.error || 'Unknown error'}
</InfoDescription>
</InfoCard>
)}
@ -385,6 +406,11 @@ const StepsContainer = styled.div`
}
`
const LanguageSelectorContainer = styled.div`
padding: 16px 24px 24px 24px;
border-top: 1px solid #f0f0f0;
`
const RightContent = styled.div`
flex: 1;
display: flex;

View File

@ -5,6 +5,7 @@
import { CheckCircle2, Circle, Loader2, XCircle } from 'lucide-react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import styled, { keyframes } from 'styled-components'
import type { MigratorProgress as MigratorProgressType, MigratorStatus } from '../types'
@ -41,14 +42,13 @@ const SpinningIcon = styled.div`
animation: ${spin} 1s linear infinite;
`
const statusTextMap: Record<MigratorStatus, string> = {
pending: '等待中',
running: '进行中',
completed: '完成',
failed: '失败'
}
export const MigratorProgressList: React.FC<Props> = ({ migrators }) => {
const { t } = useTranslation()
const getStatusText = (status: MigratorStatus): string => {
return t(`migration.status.${status}`)
}
return (
<Container>
<List>
@ -58,7 +58,7 @@ export const MigratorProgressList: React.FC<Props> = ({ migrators }) => {
<StatusIcon status={migrator.status} />
<ItemName>{migrator.name}</ItemName>
</ItemLeft>
<ItemStatus status={migrator.status}>{migrator.error || statusTextMap[migrator.status]}</ItemStatus>
<ItemStatus status={migrator.status}>{migrator.error || getStatusText(migrator.status)}</ItemStatus>
</ListItem>
))}
</List>

View File

@ -9,6 +9,7 @@ import '@ant-design/v5-patch-for-react-19'
import { loggerService } from '@logger'
import { createRoot } from 'react-dom/client'
import { initI18n } from './i18n'
import MigrationApp from './MigrationApp'
// Initialize logger for this window
@ -16,4 +17,7 @@ loggerService.initWindowSource('MigrationV2')
const root = createRoot(document.getElementById('root') as HTMLElement)
root.render(<MigrationApp />)
// Wait for i18n to be fully initialized before rendering
initI18n().then(() => {
root.render(<MigrationApp />)
})

View File

@ -0,0 +1,43 @@
/**
* i18n initialization for migration window
* Detects system language independently without relying on preferenceService
*/
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import { enUS, zhCN } from './locales'
/**
* Detect system language independently
* Rule: If system language contains 'zh', use Chinese, otherwise use English
*/
function detectLanguage(): 'zh-CN' | 'en-US' {
const browserLang = navigator.language || navigator.languages?.[0] || 'en-US'
// If contains 'zh' (zh, zh-CN, zh-TW, zh-HK, etc.), use Chinese
return browserLang.toLowerCase().includes('zh') ? 'zh-CN' : 'en-US'
}
const language = detectLanguage()
/**
* Initialize i18n asynchronously
* Must be called and awaited before rendering components
*/
const initI18n = async () => {
await i18n.use(initReactI18next).init({
resources: {
'zh-CN': { translation: zhCN },
'en-US': { translation: enUS }
},
lng: language,
fallbackLng: 'en-US',
interpolation: {
escapeValue: false
}
})
}
export default i18n
export { initI18n }

View File

@ -0,0 +1,139 @@
/**
* Migration window translations
* Supports Chinese (zh-CN) and English (en-US)
*/
export const zhCN = {
migration: {
title: '数据迁移向导',
stages: {
introduction: '介绍',
backup: '备份',
migration: '迁移',
completed: '完成'
},
buttons: {
cancel: '取消',
next: '下一步',
create_backup: '创建备份',
confirm_backup: '我已备份,开始迁移',
start_migration: '开始迁移',
confirm: '确定',
restart: '重启应用',
retry: '重新尝试',
close: '关闭应用',
backing_up: '正在备份...',
migrating: '迁移进行中...'
},
status: {
pending: '等待中',
running: '进行中',
completed: '完成',
failed: '失败'
},
introduction: {
title: '将数据迁移到新的架构中',
description_1:
'Cherry Studio对数据的存储和使用方式进行了重大重构在新的架构下效率和安全性将会得到极大提升。',
description_2: '数据必须进行迁移,才能在新版本中使用。',
description_3: '我们会指导你完成迁移,迁移过程不会损坏原来的数据,你随时可以取消迁移,并继续使用旧版本。'
},
backup_required: {
title: '创建数据备份',
description: '迁移前必须创建数据备份以确保数据安全。请选择备份位置或确认已有最新备份。'
},
backup_progress: {
title: '准备数据备份',
description: '请选择备份位置,保存后等待备份完成。'
},
backup_confirmed: {
title: '备份完成',
description: '数据备份已完成,现在可以安全地开始迁移。'
},
migration: {
title: '正在迁移数据...'
},
migration_completed: {
title: '数据迁移完成!',
description: '所有数据已成功迁移到新架构,请点击确定继续。'
},
completed: {
title: '迁移完成',
description: '数据已成功迁移,重启应用后即可正常使用。'
},
error: {
title: '迁移失败',
description: '迁移过程遇到错误,您可以重新尝试或继续使用之前版本(原始数据完好保存)。',
error_prefix: '错误信息:'
}
}
}
export const enUS = {
migration: {
title: 'Data Migration Wizard',
stages: {
introduction: 'Introduction',
backup: 'Backup',
migration: 'Migration',
completed: 'Completed'
},
buttons: {
cancel: 'Cancel',
next: 'Next',
create_backup: 'Create Backup',
confirm_backup: 'I Have Backup, Start Migration',
start_migration: 'Start Migration',
confirm: 'OK',
restart: 'Restart App',
retry: 'Retry',
close: 'Close App',
backing_up: 'Backing up...',
migrating: 'Migrating...'
},
status: {
pending: 'Pending',
running: 'Running',
completed: 'Completed',
failed: 'Failed'
},
introduction: {
title: 'Migrate Data to New Architecture',
description_1:
'Cherry Studio has undergone a major refactoring of data storage and usage. The new architecture will greatly improve efficiency and security.',
description_2: 'Data migration is required to use the new version.',
description_3:
'We will guide you through the migration process. The migration will not damage your original data, and you can cancel at any time and continue using the old version.'
},
backup_required: {
title: 'Create Data Backup',
description:
'A data backup must be created before migration to ensure data safety. Please select a backup location or confirm you have a recent backup.'
},
backup_progress: {
title: 'Preparing Data Backup',
description: 'Please select a backup location, save, and wait for the backup to complete.'
},
backup_confirmed: {
title: 'Backup Completed',
description: 'Data backup has been completed. You can now safely start the migration.'
},
migration: {
title: 'Migrating Data...'
},
migration_completed: {
title: 'Data Migration Completed!',
description: 'All data has been successfully migrated to the new architecture. Please click OK to continue.'
},
completed: {
title: 'Migration Completed',
description: 'Data has been successfully migrated. The application will work normally after restart.'
},
error: {
title: 'Migration Failed',
description:
'An error occurred during migration. You can retry or continue using the previous version (original data is intact).',
error_prefix: 'Error: '
}
}
}