国际化:让扩展说多国语言
本教程共 45 篇 · 第 19 篇 · 更新于 2026-08-13 · 约 3 分钟阅读
本节目标:学会让扩展支持多语言:原生
browser.i18n怎么搭,@wxt-dev/i18n模块又带来了哪些便利(简单格式、类型安全、复数),以及为什么「应用内切语言」需要额外设计。
国际化(i18n)要解决两件事:让 manifest 里的扩展名、描述跟着浏览器语言变;让界面文案跟着浏览器语言变。WXT 对两条路线都支持得很好。
原生方案:_locales 目录
最基础的做法分三步:
- manifest 声明默认语言:
export default defineConfig({
manifest: { default_locale: "en" },
});
- 在
public/下建语言目录,每个语言一个messages.json:
public/_locales/
en/messages.json
zh_CN/messages.json
文件里是浏览器规定的「verbose 格式」——每个 key 是一个对象,message 字段存译文,description 是给翻译者的说明:
{
"helloWorld": {
"message": "Hello world!",
"description": "首页问候语"
}
}
- 代码里取文案:
browser.i18n.getMessage("helloWorld");
manifest 里的名字和描述也能本地化,用 __MSG_key__ 占位符:
export default defineConfig({
manifest: {
name: "__MSG_extName__",
description: "__MSG_extDescription__",
default_locale: "en",
},
});
原生方案有个大优点:翻译随扩展一起打包,同步加载、零配置,manifest 和 CSS 里的文本也能本地化。它最大的缺点是语言跟着浏览器走,用户不能在扩展里单独切换语言。官方文档明确说这是原生 API 和所有基于它的封装共有的限制。
升级体验:@wxt-dev/i18n 模块
原生格式写起来啰嗦,嵌套结构也不好组织。WXT 官方推荐基于原生 API 的 @wxt-dev/i18n 模块,安装三步:
pnpm i @wxt-dev/i18n
export default defineConfig({
modules: ["@wxt-dev/i18n/module"],
manifest: { default_locale: "en" },
});
然后在 srcDir/locales/ 下建语言文件,文件名必须和 default_locale 一致:
# src/locales/en.yml
helloWorld: Hello world!
代码里用自动导入的 i18n 对象(或 import { i18n } from '#i18n'):
i18n.t("helloWorld"); // "Hello world!"
相比原生方案,这个模块带来四样东西:
1. 简单消息格式。支持 .yml / .json / .toml 等多种文件,key 可以嵌套:
welcome:
title: Welcome to XYZ
dialogs:
confirmation:
title: "Are you sure?"
i18n.t("welcome.title"); // 嵌套 key 用点号访问
2. 占位符。支持原生风格的 $1–$9,也支持命名占位符:
hello: Hello $1!
welcome: Hello {name}, welcome to {appName}!
i18n.t("hello", ["Ada"]);
i18n.t("welcome", { name: "Ada", appName: "WXT" });
3. 复数形式。同一个 key 按数量选文案:
items:
0: No items
1: 1 item
n: $1 items
i18n.t("items", 0); // "No items"
i18n.t("items", 1); // "1 item"
i18n.t("items", 2); // "2 items"
4. 类型安全。运行 wxt prepare 或构建命令时,模块会根据默认语言生成类型文件,i18n.t 的 key 参数被限定为已有键。写错 key、漏翻译,编译期就报错。编辑器方面,配好 I18n Ally 插件(官方文档提到的是 VS Code 版,JetBrains 也有对应插件)后还能跳转定义、行内预览译文。
Note从原生格式迁移不需要改文件:verbose 格式的
messages.json直接挪到src/locales/下就能用,模块完全兼容。
对照案例:mkext 的双词典方案
生产项目 mkext 没走 @wxt-dev/i18n,而是用「双词典 + 自定义 Hook」实现了应用内切语言。它的 src/locales/en.json 和 zh_CN.json 各存一份完整词典,TS 层用英文词典推导 key 类型:
import enMessages from "~/locales/en.json";
import zhMessages from "~/locales/zh_CN.json";
type Messages = typeof enMessages;
type MessageKey = keyof Messages;
const DICTIONARIES = {
en: enMessages,
zh: zhMessages,
} as const;
const translate = (locale: Locale, key: MessageKey): string =>
DICTIONARIES[locale]?.[key]?.message ?? DICTIONARIES.en[key]?.message ?? key;
关键点是 DICTIONARIES 的类型标注:中文词典缺键或改名,typecheck 直接失败,把「漏翻译」从运行时问题变成编译期问题。
语言偏好存在存储里(§18),没设置时跟随浏览器语言:
const detectLocale = (): Locale => {
const uiLanguage = browser.i18n.getUILanguage?.() ?? "en";
return LOCALES.find((locale) =>
uiLanguage.toLowerCase().startsWith(locale),
) ?? "en";
};
再配合 React Hook useI18n:读取存储里的语言偏好,返回 { locale, t, setLocale }。这套方案绕开了原生 API「语言只能跟着浏览器」的限制,代价是词典完全由自己维护,也没有复数、占位符这些现成能力。
小结
- 原生
browser.i18n+_locales零依赖,但格式啰嗦、语言跟随浏览器。 @wxt-dev/i18n模块带来简单格式、嵌套键、占位符、复数和类型安全,是官方推荐路线。- 要支持应用内切语言,得自己加一层「存储偏好 + 本地词典」,mkext 的双词典方案可以参考。