首页 / WXT 浏览器扩展框架教程 / 国际化:让扩展说多国语言

WXT 浏览器扩展框架教程

国际化:让扩展说多国语言

本教程共 45 篇 · 第 19 篇 · 更新于 2026-08-13 · 约 3 分钟阅读

WXT国际化i18nlocales多语言类型安全

本节目标:学会让扩展支持多语言:原生 browser.i18n 怎么搭,@wxt-dev/i18n 模块又带来了哪些便利(简单格式、类型安全、复数),以及为什么「应用内切语言」需要额外设计。

国际化(i18n)要解决两件事:让 manifest 里的扩展名、描述跟着浏览器语言变;让界面文案跟着浏览器语言变。WXT 对两条路线都支持得很好。

原生方案:_locales 目录

最基础的做法分三步:

  1. manifest 声明默认语言
export default defineConfig({
  manifest: { default_locale: "en" },
});
  1. public/ 下建语言目录,每个语言一个 messages.json
public/_locales/
   en/messages.json
   zh_CN/messages.json

文件里是浏览器规定的「verbose 格式」——每个 key 是一个对象,message 字段存译文,description 是给翻译者的说明:

{
  "helloWorld": {
    "message": "Hello world!",
    "description": "首页问候语"
  }
}
  1. 代码里取文案
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.jsonzh_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 的双词典方案可以参考。