HumansXドキュメントセンター 公式サイトへ ↗

国際化システムドキュメント

i18n システム概要

フロントエンドでは React Context をベースとした国際化システムを採用しており、中国語と英語のバイリンガル切り替え、型安全な翻訳キー、言語の動的読み込み、ユーザー設定の永続保存をサポートしています。

️ システムアーキテクチャ

コアコンポーネント

┌─────────────────────────────────────────────────────────────┐
│                    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>
  )
}

永続ストレージ:localStorage の管理

サーバーサイドレンダリングへの対応

/**
 * 从本地存储获取保存的语言偏好
 */
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()
  })
})

国際化システムはフロントエンドアプリケーションに完全な多言語サポートを提供し、型安全な設計と永続ストレージによって、優れたユーザー体験と開発者体験を実現します。