跳到主要内容

9. i18n 与主题

目标:为中英文支持添加新的翻译键,并理解如何定制 Tailwind v4 主题。

国际化(i18n)

文件src/i18n.ts

翻译结构

const translations = {
'zh-CN': {
'app.title': 'FreeBCI DAQ',
'app.heading': '脑电信号采集与实时频域分析',
},
'en-US': {
'app.title': 'FreeBCI DAQ',
'app.heading': 'EEG Acquisition & Realtime Spectral Analysis',
},
} as const;

export type TranslationKey = keyof (typeof translations)['zh-CN'];

export function t(locale: Locale, key: TranslationKey, values: Record<string, string | number> = {}): string {
let message: string = translations[locale][key];
for (const [name, value] of Object.entries(values)) {
message = message.split(`{${name}}`).join(String(value));
}
return message;
}

添加一个新翻译键

  1. 两个语言块 中都加入这个 key:
// en-US
'myPanel.eyebrow': 'My Feature',
'myPanel.title': 'My Panel Title',
'myPanel.description': 'This panel shows {count} items.',

// zh-CN
'myPanel.eyebrow': '我的功能',
'myPanel.title': '我的面板标题',
'myPanel.description': '此面板显示 {count} 个项目。',
  1. 在组件中使用它:
import { t } from '../i18n';
import type { Locale } from '../i18n';

export function MyPanel({ locale }: { locale: Locale }) {
return (
<Card>
<CardHeader
eyebrow={t(locale, 'myPanel.eyebrow')}
title={t(locale, 'myPanel.title')}
/>
<CardBody>
<p>{t(locale, 'myPanel.description', { count: 42 })}</p>
</CardBody>
</Card>
);
}

命名约定

{module}.{subKey}

'app.title'
'hardware.eyebrow'
'connection.openSerial'
'filter.paramHpCutoffHz'
'focus.phaseLabel'
'ai.modelSettingsTitle'

TypeScript 约束

TranslationKey 来自 zh-CN 的 key 集合。
如果你只在 zh-CN 里加了 key,却忘了 en-US,TypeScript 未必会直接报错,但英文运行时会得到 undefined。所以一定要双语同时维护。

带参数的字符串

'diagnostics.duration': 'Duration {duration} ms',
'focus.outputWindowSecondsLabel': 'User output {seconds}s focus state',

t(locale, 'diagnostics.duration', { duration: 234 });

locale 如何切换

locale 目前保存在 App.tsx 的 React state 中,由底部状态栏的语言切换按钮控制。默认值是 zh-CN,并不会持久化到 localStorage。

const [locale, setLocale] = useState<Locale>(DEFAULT_LOCALE);

useEffect(() => {
document.documentElement.lang = locale;
document.title = t(locale, 'app.title');
}, [locale]);

主题(Tailwind v4)

文件src/styles.css

主题变量

Tailwind v4 使用 CSS @theme 来声明设计 Token:

@import "tailwindcss";

@theme {
--color-paper: #f8f8f9;
--color-card: #ffffff;
--color-surface-2: #ececed;
--color-ink: #111111;
--color-meta: #525252;
--color-hint: #8a8a8a;
--color-hairline: #d4d4d8;
--color-accent: #0e7490;
--color-accent-soft: #e0f7fa;
--color-success: #047857;
--color-warn: #b45309;
--color-error: #b91c1c;
--color-grid: #e8e8eb;
--color-led-off: #c4c4c8;

--font-sans: "Inter", ui-sans-serif, system-ui, ...;
--font-mono: "JetBrains Mono", ui-monospace, ...;
--font-serif: "Instrument Serif", ui-serif, ...;

--radius-sm: 2px;
--radius-md: 4px;
}

在组件中使用这些 token

<div className="bg-card text-ink border border-hairline" />
<span className="text-accent font-mono" />
<button className="bg-accent text-white rounded-sm" />

添加一个新的主题色

@theme {
--color-brand-purple: #7c3aed;
}

然后在组件中用:

className="bg-brand-purple text-white"

字体

字体通过 @fontsource 包在 src/main.tsx 中引入:

import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/instrument-serif/400.css';
import '@fontsource/jetbrains-mono/400.css';
import '@fontsource/jetbrains-mono/500.css';

这些字体会随着 Vite 构建一起打包,不依赖外部 CDN。

UI 原语

目录src/components/ui/

组件用途关键 props
Card区块容器as, ariaLabelledBy
CardHeader卡片头部eyebrow, title, titleId, trailing
CardBody卡片内容区className
Button按钮variant, size
NumberInput数字输入min, max, step, value, onChange
TextInput文本输入标准 input props
Checkbox勾选框checked, onChange
ToggleSwitch开关checked, onChange
StatusDot状态点tone, pulse
Field表单字段容器label, children
LanguageToggle中英切换locale, onToggle

Card 结构模式

import { Card, CardHeader, CardBody } from './ui';

<Card ariaLabelledBy="my-panel-title">
<CardHeader
eyebrow="Category"
title="Panel Title"
titleId="my-panel-title"
trailing={<Button size="sm">Action</Button>}
/>
<CardBody>
{/* Panel content */}
</CardBody>
</Card>

规则:

  • Card 上设置 ariaLabelledBy
  • CardHeader 上设置对应 titleId
  • eyebrow 用于标题上方的小标签
  • trailing 用于头部右侧操作按钮

常见错误

  1. 只给一种语言加了翻译键
  2. 写死英文字符串,没有通过 t(locale, key) 输出
  3. 忘记给 translationsas const
  4. 使用了不存在的 Tailwind token 或无约束的任意样式值

接下来

编写和运行测试