第 41 章 国际化 i18n 与字体加载
本章目标
- 前端接入
react-i18next,运行时切换语言。 - 文案放到 JSON,支持插值和复数。
- 字体:西文 Inter、中文 Noto Sans SC 的加载与回退。
- 系统语言自动检测。
一、依赖
pnpm add i18next react-i18next i18next-browser-languagedetector
二、资源结构
src/i18n/
├── index.ts
└── locales/
├── zh.json
├── en.json
└── ja.json
// zh.json
{
"common": { "play": "播放", "pause": "暂停", "search": "搜索", "settings": "设置" },
"player": { "nowPlaying": "正在播放", "noSong": "未选择歌曲", "queueCount": "{{count}} 首歌" },
"library": { "import": "导入音乐", "scanning": "扫描中…({{done}}/{{total}})" }
}
三、初始化
// src/i18n/index.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import zh from "./locales/zh.json";
import en from "./locales/en.json";
import ja from "./locales/ja.json";
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: { zh: { translation: zh }, en: { translation: en }, ja: { translation: ja } },
fallbackLng: "en",
interpolation: { escapeValue: false },
detection: { order: ["localStorage","navigator"], caches: ["localStorage"] },
});
export default i18n;
main.tsx 里 import "./i18n"。
四、使用
import { useTranslation } from "react-i18next";
function PlayButton() {
const { t } = useTranslation();
return <button>{t("common.play")}</button>;
}
function ScanStatus({ done, total }: { done: number; total: number }) {
const { t } = useTranslation();
return <div>{t("library.scanning", { done, total })}</div>;
}
五、运行时切换
import i18n from "@/i18n";
i18n.changeLanguage("en");
存偏好到 Rust 侧的 settings 表:
async function setLanguage(lang: string) {
await i18n.changeLanguage(lang);
await commands.settingsSet("lang", lang);
}
启动时读:
const lang = await commands.settingsGet("lang");
if (lang) i18n.changeLanguage(lang);
六、字体
包内字体
把 TTF/WOFF2 放到 public/fonts/。CSS:
@font-face {
font-family: "Inter";
src: url("/fonts/Inter-Variable.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: "Noto Sans SC";
src: url("/fonts/NotoSansSC-Regular.woff2") format("woff2");
font-weight: 400;
font-display: swap;
}
body {
font-family: "Inter", "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
font-display: swap 让系统字体先渲染,避免 FOIT(隐形文本闪烁)。
仅按需加载
10MB 的中文 WOFF2 很贵。可以只在用户选中文时加载:
async function ensureChineseFont() {
if (document.fonts.check("16px 'Noto Sans SC'")) return;
const font = new FontFace("Noto Sans SC", "url(/fonts/NotoSansSC-Regular.woff2)");
await font.load();
document.fonts.add(font);
}
七、Rust 侧错误信息
Rust 返回的错误要不要本地化?CloudTone 选不做:
- 错误信息发给用户友好的封装层(前端 toast),英文原文保留在 log 供调试。
- 前端根据
error.code查 i18n 表。
#[derive(thiserror::Error, Debug, serde::Serialize, specta::Type)]
#[serde(tag = "code", rename_all = "camelCase")]
pub enum UserError {
#[error("file not found")] FileNotFound,
#[error("unsupported format")] UnsupportedFormat,
#[error("network")] Network,
}
前端:
catch (e: any) {
toast.error(t(`errors.${e.code ?? "unknown"}`));
}
八、系统语言检测
detection: { order: ["localStorage","navigator"], caches: ["localStorage"] }
navigator 会用 navigator.language。Tauri 上它等于 WebView 的语言(通常随系统)。想严格跟系统:
#[tauri::command] #[specta::specta]
pub fn system_locale() -> String {
sys_locale::get_locale().unwrap_or("en-US".into())
}
前端调用 system_locale() 做兜底。
本章小结
- i18next 的生态最成熟,React 集成优雅。
- 字体按需加载节省安装包。
- 错误码分离展示文案,扩展语言零成本。
动手时刻
- 切换到 English,看界面变化。
- 首次启动时自动跟随系统语言。
下一章:插件系统设计。