国際化システムのドキュメント
🌍 i18n システムの概要
フロントエンドは React Context ベースの国際化システムを採用しており、中国語/英語の 2 言語切り替え、型安全な翻訳キー、言語の動的読み込み、ユーザー設定の永続化に対応しています。
🏗️ システムアーキテクチャ
コアコンポーネント
┌─────────────────────────────────────────────────────────────┐
│ i18n 系统架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Config │ │ Context │ │ Storage │ │
│ │ (配置) │ │ (上下文) │ │ (持久化) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ i18nProvider │ │
│ │ (语言状态管理和切换逻辑) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 语言文件加载器 │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ zh.ts │ │ en.ts │ │ │
│ │ │ (中文) │ │ (英文) │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
⚙️ 設定システム
言語設定の定義
lib/i18n/config.ts:
export type SupportedLocale = 'zh' | 'en'
export interface LocaleConfig {
code: SupportedLocale
name: string
nativeName: string
flag: string
}
export const SUPPORTED_LOCALES: Record<SupportedLocale, LocaleConfig> = {
zh: {
code: 'zh',
name: 'Chinese',
nativeName: '中文',
flag: '🇨🇳'
},
en: {
code: 'en',
name: 'English',
nativeName: 'English',
flag: '🇺🇸'
}
}
export const DEFAULT_LOCALE: SupportedLocale = 'zh'
export const LOCALE_STORAGE_KEY = 'preferred-locale'
/**
* 获取浏览器首选语言
*/
export function getBrowserLocale(): SupportedLocale {
if (typeof window === 'undefined') return DEFAULT_LOCALE
const browserLang = navigator.language.toLowerCase()
if (browserLang.startsWith('zh')) return 'zh'
if (browserLang.startsWith('en')) return 'en'
return DEFAULT_LOCALE
}
/**
* 获取初始语言设置
* 优先级:本地存储 > 浏览器语言 > 默认语言
*/
export function getInitialLocale(): SupportedLocale {
return getSavedLocale() || getBrowserLocale()
}
言語ファイルの構成
lib/i18n/locales/zh.ts(中国語):
export const zh = {
// 页面文本
page: {
title: 'ASR-LLM-TTS 语音对话助手',
subtitle: '智能语音交互系统',
description: '基于大语言模型的语音对话助手,支持实时语音识别、智能对话和语音合成'
},
// 聊天界面
chat: {
placeholder: '输入你的问题...',
send: '发送',
interrupt: '中断',
listening: '正在听...',
thinking: '思考中...',
speaking: '回复中...',
voiceMode: '语音模式',
keyboardMode: '键盘模式',
clearChat: '清空对话',
exportChat: '导出对话'
},
// 系统状态
status: {
connecting: '连接中...',
connected: '已连接',
disconnected: '连接断开',
reconnecting: '重新连接中...',
error: '连接错误'
},
// 工具调用
tools: {
calling: '正在调用工具...',
success: '工具执行成功',
error: '工具执行失败',
calculator: '计算器',
musicPlayer: '音乐播放器',
armControl: '机械臂控制'
},
// 设置界面
settings: {
options: '设置',
language: '语言',
theme: '主题',
themes: {
default: '默认主题',
alternative: '备选主题',
happy: '欢快主题'
},
audio: '音频设置',
volume: '音量',
microphone: '麦克风'
},
// 错误消息
errors: {
connectionFailed: 'WebSocket连接失败',
networkError: '网络错误',
serverError: '服务器错误',
audioError: '音频设备错误',
microphonePermission: '麦克风权限被拒绝'
},
// 通用文本
common: {
yes: '是',
no: '否',
ok: '确定',
cancel: '取消',
save: '保存',
load: '加载',
delete: '删除',
edit: '编辑',
retry: '重试',
loading: '加载中...',
success: '成功',
failed: '失败'
}
} as const
lib/i18n/locales/en.ts(英語):
export const en = {
page: {
title: 'ASR-LLM-TTS Voice Assistant',
subtitle: 'Intelligent Voice Interaction System',
description: 'AI-powered voice assistant with real-time speech recognition, intelligent conversation, and speech synthesis'
},
chat: {
placeholder: 'Type your question...',
send: 'Send',
interrupt: 'Interrupt',
listening: 'Listening...',
thinking: 'Thinking...',
speaking: 'Speaking...',
voiceMode: 'Voice Mode',
keyboardMode: 'Keyboard Mode',
clearChat: 'Clear Chat',
exportChat: 'Export Chat'
},
status: {
connecting: 'Connecting...',
connected: 'Connected',
disconnected: 'Disconnected',
reconnecting: 'Reconnecting...',
error: 'Connection Error'
},
tools: {
calling: 'Calling tool...',
success: 'Tool executed successfully',
error: 'Tool execution failed',
calculator: 'Calculator',
musicPlayer: 'Music Player',
armControl: 'Arm Control'
},
settings: {
options: 'Settings',
language: 'Language',
theme: 'Theme',
themes: {
default: 'Default Theme',
alternative: 'Alternative Theme',
happy: 'Happy Theme'
},
audio: 'Audio Settings',
volume: 'Volume',
microphone: 'Microphone'
},
errors: {
connectionFailed: 'WebSocket connection failed',
networkError: 'Network error',
serverError: 'Server error',
audioError: 'Audio device error',
microphonePermission: 'Microphone permission denied'
},
common: {
yes: 'Yes',
no: 'No',
ok: 'OK',
cancel: 'Cancel',
save: 'Save',
load: 'Load',
delete: 'Delete',
edit: 'Edit',
retry: 'Retry',
loading: 'Loading...',
success: 'Success',
failed: 'Failed'
}
} as const
🔧 Context の実装
i18n Context
lib/i18n/context.tsx:
'use client'
import React, { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
import { type SupportedLocale, getInitialLocale, saveLocale } from './config'
import { zh } from './locales/zh'
import { en } from './locales/en'
// 语言资源映射
const resources = { zh, en } as const
// 推断翻译键的类型
type TranslationKeys = typeof zh
type NestedKeyOf<T> = T extends object
? { [K in keyof T]: K extends string
? T[K] extends object
? `${K}.${NestedKeyOf<T[K]>}`
: K
: never
}[keyof T]
: never
type TranslationKey = NestedKeyOf<TranslationKeys>
// Context类型定义
interface I18nContextType {
locale: SupportedLocale
setLocale: (locale: SupportedLocale) => void
t: TranslationKeys
tKey: (key: TranslationKey) => string
}
const I18nContext = createContext<I18nContextType | null>(null)
interface I18nProviderProps {
children: ReactNode
defaultLocale?: SupportedLocale
}
export function I18nProvider({ children, defaultLocale }: I18nProviderProps) {
const [locale, setLocaleState] = useState<SupportedLocale>(defaultLocale || getInitialLocale())
// 加载语言资源
const translations = resources[locale]
// 切换语言函数
const setLocale = (newLocale: SupportedLocale) => {
setLocaleState(newLocale)
saveLocale(newLocale)
// 更新HTML lang属性
if (typeof document !== 'undefined') {
document.documentElement.lang = newLocale
}
}
// 通过键路径获取翻译文本
const tKey = (key: TranslationKey): string => {
const keys = key.split('.')
let value: any = translations
for (const k of keys) {
value = value?.[k]
if (value === undefined) {
console.warn(`Translation key "${key}" not found for locale "${locale}"`)
return key
}
}
return typeof value === 'string' ? value : key
}
const contextValue: I18nContextType = {
locale,
setLocale,
t: translations,
tKey,
}
return (
<I18nContext.Provider value={contextValue}>
{children}
</I18nContext.Provider>
)
}
export function useI18n(): I18nContextType {
const context = useContext(I18nContext)
if (!context) {
throw new Error('useI18n must be used within an I18nProvider')
}
return context
}
// 类型安全的翻译Hook
export function useTranslation() {
const { t, tKey, locale } = useI18n()
return {
t,
tKey,
locale,
// 格式化翻译(支持插值)
tf: (key: TranslationKey, params?: Record<string, string | number>) => {
let text = tKey(key)
if (params) {
Object.entries(params).forEach(([key, value]) => {
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value))
})
}
return text
}
}
}
🎨 コンポーネントへの統合
言語切り替えコンポーネント
components/language-switcher.tsx:
import React from 'react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useI18n } from '@/lib/i18n/context'
import { SUPPORTED_LOCALES, type SupportedLocale } from '@/lib/i18n/config'
interface LanguageSwitcherProps {
variant?: 'default' | 'compact'
className?: string
}
export function LanguageSwitcher({ variant = 'default', className }: LanguageSwitcherProps) {
const { locale, setLocale, t } = useI18n()
const handleLocaleChange = (newLocale: SupportedLocale) => {
setLocale(newLocale)
}
const currentLocale = SUPPORTED_LOCALES[locale]
if (variant === 'compact') {
return (
<div className="px-2 py-1">
{Object.values(SUPPORTED_LOCALES).map((localeConfig) => (
<button
key={localeConfig.code}
onClick={() => handleLocaleChange(localeConfig.code)}
className={cn(
"block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900",
locale === localeConfig.code && "bg-gray-100 text-gray-900"
)}
>
<div className="flex items-center">
<span className="mr-2">{localeConfig.flag}</span>
{localeConfig.nativeName}
</div>
</button>
))}
</div>
)
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className={className}>
<span className="mr-2">{currentLocale.flag}</span>
{currentLocale.nativeName}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.values(SUPPORTED_LOCALES).map((localeConfig) => (
<DropdownMenuItem
key={localeConfig.code}
onClick={() => handleLocaleChange(localeConfig.code)}
className="cursor-pointer"
>
<span className="mr-2">{localeConfig.flag}</span>
{localeConfig.nativeName}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
使用例
import { useI18n, useTranslation } from '@/lib/i18n/context'
export function ChatInterface() {
const { t } = useI18n()
const { tf } = useTranslation()
return (
<div>
{/* 直接使用翻译对象 */}
<h1>{t.page.title}</h1>
<p>{t.chat.placeholder}</p>
{/* 使用键路径 */}
<button>{tf('chat.send')}</button>
{/* 带参数的翻译 */}
<p>{tf('status.connecting', { server: 'localhost:8000' })}</p>
</div>
)
}
💾 永続化ストレージ
ローカルストレージの管理
/**
* 从本地存储获取保存的语言偏好
*/
export function getSavedLocale(): SupportedLocale | null {
if (typeof window === 'undefined') return null
try {
const saved = localStorage.getItem(LOCALE_STORAGE_KEY)
if (saved && Object.keys(SUPPORTED_LOCALES).includes(saved)) {
return saved as SupportedLocale
}
} catch (error) {
console.warn('Failed to read locale from localStorage:', error)
}
return null
}
/**
* 保存语言偏好到本地存储
*/
export function saveLocale(locale: SupportedLocale): void {
if (typeof window === 'undefined') return
try {
localStorage.setItem(LOCALE_STORAGE_KEY, locale)
} catch (error) {
console.warn('Failed to save locale to localStorage:', error)
}
}
サーバーサイドレンダリングへの対応
// app/layout.tsx
import { I18nProvider } from '@/lib/i18n/context'
import { getInitialLocale } from '@/lib/i18n/config'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
// SSR友好的初始语言设置
const initialLocale = typeof window !== 'undefined'
? getInitialLocale()
: 'zh' // 服务器端默认使用中文
return (
<html lang={initialLocale}>
<body>
<I18nProvider defaultLocale={initialLocale}>
{children}
</I18nProvider>
</body>
</html>
)
}
🔒 型安全
翻訳キーの型推論
// 自动推断所有翻译键的类型
type TranslationKey =
| 'page.title'
| 'page.subtitle'
| 'chat.placeholder'
| 'chat.send'
| 'status.connected'
// ... 所有其他键
// 使用时提供完整的类型提示和检查
function useTypedTranslation() {
const { tKey } = useI18n()
return {
get: (key: TranslationKey) => tKey(key),
// 支持插值的翻译函数
format: (key: TranslationKey, params?: Record<string, string | number>) => {
let text = tKey(key)
if (params) {
Object.entries(params).forEach(([key, value]) => {
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value))
})
}
return text
}
}
}
コンパイル時のキーチェック
// 开发模式下检查翻译键完整性
if (process.env.NODE_ENV === 'development') {
const zhKeys = flattenKeys(zh)
const enKeys = flattenKeys(en)
const missingInEn = zhKeys.filter(key => !enKeys.includes(key))
const missingInZh = enKeys.filter(key => !zhKeys.includes(key))
if (missingInEn.length > 0) {
console.warn('Missing English translations:', missingInEn)
}
if (missingInZh.length > 0) {
console.warn('Missing Chinese translations:', missingInZh)
}
}
🧪 テストのサポート
テスト用ユーティリティ関数
// 测试中的i18n Mock
export function createMockI18nContext(locale: SupportedLocale = 'zh') {
const translations = resources[locale]
return {
locale,
setLocale: jest.fn(),
t: translations,
tKey: (key: string) => {
const keys = key.split('.')
let value: any = translations
for (const k of keys) {
value = value?.[k]
}
return typeof value === 'string' ? value : key
}
}
}
// 测试组件包装器
export function I18nTestWrapper({
children,
locale = 'zh'
}: {
children: React.ReactNode
locale?: SupportedLocale
}) {
return (
<I18nProvider defaultLocale={locale}>
{children}
</I18nProvider>
)
}
ユニットテストの例
import { render, screen } from '@testing-library/react'
import { I18nTestWrapper } from '../test-utils'
import { ChatInterface } from '../chat-interface'
describe('ChatInterface i18n', () => {
it('should display Chinese text by default', () => {
render(
<I18nTestWrapper locale="zh">
<ChatInterface />
</I18nTestWrapper>
)
expect(screen.getByText('发送')).toBeInTheDocument()
expect(screen.getByPlaceholderText('输入你的问题...')).toBeInTheDocument()
})
it('should display English text when locale is en', () => {
render(
<I18nTestWrapper locale="en">
<ChatInterface />
</I18nTestWrapper>
)
expect(screen.getByText('Send')).toBeInTheDocument()
expect(screen.getByPlaceholderText('Type your question...')).toBeInTheDocument()
})
})
国際化システムは、フロントエンドアプリケーションに完全な多言語対応を提供します。型安全な設計と永続化ストレージにより、良好なユーザー体験と開発者体験を確保しています。
ドキュメント