跳转至

@bams-app/i18n

📄 创建: 刘泽伟 2026-08-04

https://doc.xpku.com/raw/tech/frontend/i18n/%E8%AF%B4%E6%98%8E.md

面向 BAMS monorepo 的运行时国际化包,重点解决这几类问题:

  • 应用运行时切换语言,不刷新页面
  • 统一同步 Ant Design VueDevExtremedayjs 的语言
  • 支持业务组件按模块拆分语言包,避免全量聚合
  • 支持局部翻译和纯 JS 翻译场景

一句话能力图

  • 应用级管理createI18nManager() 负责语言状态、模块注册、缓存、持久化和 fallback
  • 模块级拆分defineI18nModule() / defineBusinessI18n() 支持同步消息和异步 loader
  • 组件级接入attachI18n() + useComponentI18n() 让业务组件接入足够轻
  • 局部翻译useLocalI18n() / LocalI18nProvider / useLocalI18nProvided() 适合弹窗、子模块、临时文案域
  • 非 Vue 场景translate()translateScoped()createScopedTranslator()createLocalTranslator()
  • 体验增强:语言持久化、别名标准化、函数式翻译、双插值语法、语言变更监听

目录架构

src/
├── index.js                    # 包级对外统一出口
├── core/                       # 核心能力层
│   ├── constants.js            # 常量定义(默认语言、支持列表、别名、注入 Symbol 等)
│   ├── manager.js              # i18n 管理器核心(状态、缓存、语言切换、模块注册等)
│   ├── composables.js          # Vue 组件侧组合式 API(useLocale、useScopedI18n、useI18nContext)
│   ├── builtin.js              # 内建模块:Antd / DevExtreme / dayjs 语言适配器
│   ├── utils.js                # 通用工具函数(对象合并、路径取值、消息格式化等)
│   └── dayjs-locale.d.ts       # dayjs 类型声明
├── components/                 # UI 组件层
│   ├── I18nProvider.vue        # Ant Design Vue ConfigProvider 包裹组件
│   ├── LocaleSwitchPanel.vue   # 语言切换面板组件
│   └── LocalI18nProvider.vue   # 局部翻译容器组件
├── business/                   # 业务组件接入层
│   └── business.js             # 业务组件简化接入(defineBusinessI18n、attachI18n、useComponentI18n、useLocalI18n)
└── internal/                   # 内部实现层
    ├── local-i18n.js           # 局部翻译共享核心逻辑
    ├── module.js               # i18n 模块定义与 bundle 解析、解包、fallback 处理
    ├── locale-resources.js     # 语言资源聚合与三方库适配应用
    ├── storage.js              # localStorage 持久化读写
    └── scope.js                # 命名空间路径拼接

架构分层说明

层级 目录/文件 职责
入口层 index.js 统一导出,保持对外 API 稳定
业务接入层 business/ 业务组件简化接入、命名空间管理
UI 组件层 components/ 语言切换、局部翻译容器等 UI 组件
核心能力层 core/ 通用组合式 API、管理器、工具函数
内部实现层 internal/ 模块解析、资源聚合、局部翻译核心等

管理器 API

const i18n = createI18nManager(options);

实例方法:

方法 说明
init() 初始化,读取持久化语言并完成首次资源加载
setLocale(locale, options?) 切换语言
loadLocale(locale) 预加载指定语言资源
registerModule(moduleOptions) 注册单个模块
registerModules(modules) 批量注册模块
unregisterModule(moduleName) 卸载单个模块(内建模块不可卸载)
unregisterModules(moduleNames) 批量卸载模块
getRegisteredModules() 获取当前已注册的所有模块名
getLocaleMessages(locale?) 获取聚合后的消息对象
onLocaleChange(listener) 监听语言切换
t(path, params?, locale?) 翻译指定路径
te(path, locale?) 判断路径是否存在翻译

实例属性:

属性 说明
state 只读响应式状态,含 localeloadingreadyavailableLocales
antdLocale 当前 Antd locale 引用

Vue 全局属性

安装 app.use(i18n) 后会自动挂载:

属性 说明
$bamsI18n i18n 管理器实例
$t 全局翻译函数
$locale 当前语言

常用组件

LocaleSwitchPanel

开箱即用的语言切换组件。

<LocaleSwitchPanel />

<LocaleSwitchPanel :locales="['zh-CN', 'en-US']" :locale-labels="{ 'zh-CN': '中文', 'en-US': 'English' }" />

Props:

参数 说明 类型 默认值
locales 显示哪些语言 Array 使用管理器中的 availableLocales,为空时退回默认支持列表
localeLabels 自定义语言显示名称 Object 内置中文 / English

LocalI18nProvider

为一个局部区域提供独立翻译上下文。

<template>
  <LocalI18nProvider :messages="localMessages" namespace="myModule">
    <template #default="{ t }">
      <div>{{ t("title") }}</div>
    </template>
  </LocalI18nProvider>
</template>

Props:

参数 说明 类型 默认值
messages 局部消息字典 Object {}
namespace 局部命名空间,不传时继承父业务组件 namespace String null
fallbackToGlobal 局部缺失时是否回退到全局 Boolean true

使用方式

1. 应用级初始化

适用场景:整站、平台、工作台根应用。

import { createI18nManager } from "@bams-app/i18n";

const i18n = createI18nManager({
  // 默认语言
  locale: "zh-CN",
  // 兜底语言(当前语言找不到翻译时使用)
  fallbackLocale: "zh-CN",
  // 是否持久化到 localStorage
  persist: true,
  // 自定义存储 key
  storageKey: "my-app-locale",
  // 初始化时预注册的模块
  modules: [],
  // 可用语言列表(会动态扩展)
  availableLocales: ["zh-CN", "en-US"],
});

app.use(i18n);
await i18n.init();

推荐再用 I18nProvider 包一下根视图,保证 Antd 文案同步刷新:

<template>
  <I18nProvider>
    <router-view />
  </I18nProvider>
</template>

<script setup>
  import { I18nProvider } from "@bams-app/i18n";
</script>

2. 注册 i18n 模块

适用场景:基础组件、平台级模块、不会依赖业务容器自动注册的包。

模块定义(同步 + 异步混合模式)

import { defineI18nModule } from "@bams-app/i18n";

export const demoI18nModule = defineI18nModule({
  name: "demo-module",
  namespace: "demoModule",
  // 同步消息(启动时立即加载)
  messages: {
    "zh-CN": {
      common: {
        confirm: "确定",
        cancel: "取消",
      },
    },
    "en-US": {
      common: {
        confirm: "Confirm",
        cancel: "Cancel",
      },
    },
  },
  // 异步加载器(按需加载,减少首屏体积)
  loaders: {
    "zh-CN": () => import("./locales/zh-CN.js"),
    "en-US": () => import("./locales/en-US.js"),
  },
});

动态注册模块

import { demoI18nModule } from "./i18n.js";

// 单个注册
await i18n.registerModule(demoI18nModule);

// 批量注册
await i18n.registerModules([module1, module2]);

在 Vue 组件中使用

import { useScopedI18n } from "@bams-app/i18n";

// 带命名空间的翻译
const { t, te, locale, setLocale } = useScopedI18n("demoModule");

// 使用:t("common.confirm") 等价于 t("demoModule.common.confirm")
console.log(t("common.confirm"));

全局翻译(不带命名空间)

import { useLocale } from "@bams-app/i18n";

const { t, te } = useLocale();

// 使用完整路径
console.log(t("demoModule.common.confirm"));

3. 业务组件极简接入

适用场景:

  • ui-xxx 业务组件
  • page-xxx 页面壳

推荐组合:

  • defineBusinessI18n
  • attachI18n
  • useComponentI18n

组件入口文件(index.js)

import ReportTeam from "./ReportTeam.vue";
import { defineBusinessI18n, attachI18n } from "@bams-app/i18n";

// 定义业务模块(自动将 kebab-case 包名转成 camelCase namespace)
const i18nModule = defineBusinessI18n({
  name: "ui-report-team",
  loaders: {
    "zh-CN": () => import("./locales/zh-CN.js"),
    "en-US": () => import("./locales/en-US.js"),
  },
});

// 自动挂载(生命周期管理 + namespace 注入)
export default attachI18n(ReportTeam, i18nModule);

组件内部使用

<script setup>
  import { useComponentI18n } from "@bams-app/i18n";

  // 自动继承 namespace,无需手动指定
  const { t } = useComponentI18n();

  // 直接使用相对路径
  console.log(t("toolbar.export"));
</script>

子孙组件使用(自动继承 namespace)

<script setup>
  import { useComponentI18n } from "@bams-app/i18n";

  // 直接获取父组件的 namespace
  const { t } = useComponentI18n();
</script>

完整指南见:guide.md

4. 局部翻译

适用场景:

  • 一个页面中的局部模块需要自带独立字典
  • 弹窗、配置面板、临时子流程不想污染全局命名空间
  • 希望局部优先,找不到时再回退到全局

方式一:useLocalI18n(当前组件内使用)

<script setup>
  import { useLocalI18n } from "@bams-app/i18n";

  // 可直接调用的翻译函数
  const t = useLocalI18n({
    messages: {
      "zh-CN": {
        title: "局部标题",
        desc: "描述信息:{name}",
      },
      "en-US": {
        title: "Local Title",
        desc: "Description: {name}",
      },
    },
    // 可选:自定义 namespace
    namespace: "myLocal",
    // 可选:找不到时是否回退到全局(默认 true)
    fallbackToGlobal: true,
  });

  // 使用方式1:直接调用
  console.log(t("title"));
  console.log(t("desc", { name: "测试" }));

  // 使用方式2:通过 .t 调用
  console.log(t.t("title"));

  // 检查翻译是否存在
  console.log(t.te("title"));
</script>

<template>
  <div>
    <h1>{{ t("title") }}</h1>
  </div>
</template>

方式二:LocalI18nProvider + useLocalI18nProvided(跨组件共享)

<!-- 父组件提供 -->
<template>
  <LocalI18nProvider :messages="localMessages" namespace="dialog">
    <template #default="{ t }">
      <div>
        <h2>{{ t("title") }}</h2>
        <ChildComponent />
      </div>
    </template>
  </LocalI18nProvider>
</template>

<script setup>
  import { LocalI18nProvider } from "@bams-app/i18n";

  const localMessages = {
    "zh-CN": { title: "对话框标题" },
    "en-US": { title: "Dialog Title" },
  };
</script>
<!-- 子孙组件消费 -->
<script setup>
  import { useLocalI18nProvided } from "@bams-app/i18n";

  // 获取父组件 provide 的局部翻译
  // 如果找不到,会自动回退到全局翻译并警告
  const t = useLocalI18nProvided();

  console.log(t("title"));
</script>

局部翻译的回退机制详解

局部翻译的查找顺序(优先级从高到低):

  1. 局部字典查找 → 当前语言的对应 key
  2. 全局回退 → 如果找不到且 fallbackToGlobal=true,查找全局翻译
  3. 兜底语言查找 → 当前语言找不到,尝试 fallbackLocale 的局部字典
<script setup>
import { useLocalI18n } from "@bams-app/i18n";

const localMessages = {
  "zh-CN": {
    localKey: "中文局部翻译",
  },
  "en-US": {
    localKey: "English local translation",
  },
};

const t = useLocalI18n({
  messages: localMessages,
  fallbackToGlobal: true, // 默认 true
});
</script>

<template>
  <div>
    <!-- 1. 优先使用局部翻译 -->
    <p>{{ t('localKey') }}</p>

    <!-- 2. 局部找不到,回退到全局 -->
    <p>{{ t('common.title') }}</p>

    <!-- 3. 都找不到,返回 key 本身 -->
    <p>{{ t('nonExistentKey') }}</p>
  </div>
</template>

useLocalI18n 的完整返回值

const translator = useLocalI18n({
  messages: localMessages,
  namespace: 'myModule',
  fallbackToGlobal: true,
});

// 可以直接调用 translator()
translator('key');

// 也可以通过属性访问:
translator.t;         // 翻译函数
translator.te;        // 检查翻译是否存在
translator.locale;    // 当前语言(响应式)
translator.setLocale; // 设置语言
translator.antdLocale;// Ant Design 的 locale 对象
translator.loading;   // 加载状态
translator.state;     // 完整的 i18n 状态
translator.namespace; // 当前 namespace

响应式的 messages 和 options

useLocalI18n 支持响应式的配置:

<script setup>
import { ref, computed } from 'vue';
import { useLocalI18n } from "@bams-app/i18n";

const isDarkMode = ref(false);

// 响应式的 messages
const dynamicMessages = computed(() => ({
  "zh-CN": {
    theme: isDarkMode.value ? "深色模式" : "浅色模式",
  },
  "en-US": {
    theme: isDarkMode.value ? "Dark Mode" : "Light Mode",
  },
}));

const t = useLocalI18n({
  messages: dynamicMessages, // 传入 ref 或 computed
});
</script>

<template>
  <div>
    <a-switch v-model:checked="isDarkMode" />
    <p>{{ t('theme') }}</p>
  </div>
</template>

5. 纯 JS 翻译

适用场景:工具函数、helper、非 Vue 模块。

获取管理器实例

import { getI18nManager } from "@bams-app/i18n";

const i18n = getI18nManager();
if (i18n) {
  console.log(i18n.t("path.to.key"));
}

全局翻译

import { translate } from "@bams-app/i18n";

const msg = translate("demoModule.common.confirm");
const msgWithParams = translate("greeting", { name: "张三" });
const msgWithLocale = translate("hello", {}, "en-US");

带命名空间的翻译

import { translateScoped } from "@bams-app/i18n";

const msg = translateScoped("reportTeam", "toolbar.export");

创建可复用的作用域翻译器

import { createScopedTranslator } from "@bams-app/i18n";

const scopedT = createScopedTranslator("reportTeam");

// 方式1:直接调用
console.log(scopedT("toolbar.export"));

// 方式2:通过 .t 调用
console.log(scopedT.t("toolbar.export"));

// 检查是否存在
console.log(scopedT.te("toolbar.export"));

创建局部翻译器(纯 JS)

import { createLocalTranslator } from "@bams-app/i18n";

const localT = createLocalTranslator({
  messages: {
    "zh-CN": { confirm: "确定", cancel: "取消" },
    "en-US": { confirm: "Confirm", cancel: "Cancel" },
  },
  namespace: "myUtils",
  fallbackToGlobal: true,
});

console.log(localT("confirm"));
console.log(localT.te("confirm"));

6. 语言切换与监听

切换语言

import { useLocale } from "@bams-app/i18n";

const { setLocale } = useLocale();

// 切换到英文
await setLocale("en-US");

// 切换到中文(支持别名)
await setLocale("zh");

监听语言变化

import { getI18nManager } from "@bams-app/i18n";

const i18n = getI18nManager();

const unsubscribe = i18n.onLocaleChange((locale, aggregate) => {
  console.log("语言已切换为:", locale);
  console.log("三方库 locale:", aggregate.antdLocale);
});

// 取消监听
unsubscribe();

7. 预加载语言资源

import { getI18nManager } from "@bams-app/i18n";

const i18n = getI18nManager();

// 预加载英文资源(提升切换体验)
await i18n.loadLocale("en-US");

8. 模板中直接使用

<template>
  <div>
    <!-- 全局翻译 -->
    <button>{{ $t("common.confirm") }}</button>

    <!-- 当前语言 -->
    <span>{{ $locale }}</span>
  </div>
</template>

五类能力

1. 运行时语言管理

  • setLocale() 切换语言时会重新聚合模块资源并同步三方库
  • init() 会读取持久化语言并完成首次加载
  • registerModule() / registerModules() 支持运行时动态注册模块
  • unregisterModule() / unregisterModules() 支持运行时动态卸载模块(引用计数自动清理)
  • onLocaleChange() 支持监听语言切换事件

2. 模块化消息组织

  • 单个模块可同时声明 messagesloaders
  • 当前语言缺失时,自动回退到 fallbackLocale
  • 模块消息会按 namespace 自动挂载,避免键冲突
  • defineBusinessI18n() 默认把 kebab-case 包名转成 camelCase namespace

3. 三方库语言同步

内建模块会自动处理:

  • Ant Design Vue locale
  • DevExtreme locale + messages
  • dayjs locale

默认内建支持:

  • zh-CN
  • en-US

4. 局部翻译与回退

  • 局部字典优先
  • 局部找不到时可回退到全局
  • 支持继承父业务组件 namespace
  • 同时支持 Vue 和纯 JS 两类局部翻译能力

5. 文案表达能力

  • 支持 {{key}}{key} 两种插值语法
  • 支持函数式翻译值:params => string
  • 支持语言别名标准化:zhzh-cnzh_cnenen-usen_us

关键边界说明

这部分建议使用方先看一遍,能少踩很多坑。

  • 不是所有 API 都返回可直接调用的函数useLocale()useScopedI18n()useComponentI18n() 返回的是对象,需要解构出 t
  • 可直接调用的 translator 主要有这些createScopedTranslator()useLocalI18n()useLocalI18nProvided()createLocalTranslator()
  • createLocalTranslator() 不是 useLocalI18n() 的完全等价物:它适合纯 JS 调用,但不提供 Vue 响应式的 locale / loading / state
  • useLocalI18nProvided() 找不到 provider 时会回退到全局翻译并给出警告

高级用法

1. 手动管理模块引用计数

import { retainI18nModule, releaseI18nModule } from "@bams-app/i18n";

// 增加引用计数并注册模块(如果未注册)
await retainI18nModule(myModule);

// 减少引用计数,计数为 0 时卸载模块
await releaseI18nModule(myModule);

2. 函数式翻译值

// 语言包文件
export default {
  greeting: params => {
    const hour = new Date().getHours();
    if (hour < 12) return `早上好,${params.name}!`;
    if (hour < 18) return `下午好,${params.name}!`;
    return `晚上好,${params.name}!`;
  },
};

// 使用
t("greeting", { name: "张三" });

3. 获取注册的模块列表

import { getI18nManager } from "@bams-app/i18n";

const i18n = getI18nManager();
const modules = i18n.getRegisteredModules();
console.log("已注册模块:", modules);

4. 获取当前语言的所有消息

import { getI18nManager } from "@bams-app/i18n";

const i18n = getI18nManager();
const messages = i18n.getLocaleMessages(); // 当前语言
const enMessages = i18n.getLocaleMessages("en-US"); // 指定语言

最佳实践

1. 模块组织

  • 按业务领域划分模块,避免单模块过大
  • 通用功能(如按钮、表单)抽取为共享模块
  • 业务组件用 defineBusinessI18n + attachI18n 简化接入

2. 性能优化

  • 常用翻译放在 messages(同步加载)
  • 不常用或体积大的翻译用 loaders(异步加载)
  • 可在用户可能切换语言前预加载 i18n.loadLocale()

3. 命名规范

  • 包名使用 kebab-case:ui-report-team
  • namespace 自动转 camelCase:uiReportTeam
  • 翻译 key 使用小写字母 + 点分隔:toolbar.export

4. 语言文件组织

locales/
  ├── zh-CN.js
  ├── en-US.js
  └── index.js (可选,汇总导出)
// zh-CN.js
export default {
  toolbar: {
    export: "导出",
    import: "导入",
  },
  table: {
    columns: {
      name: "名称",
      date: "日期",
    },
  },
};