基于 @ice/plugin-intl 的 ice.js 国际化(i18n)实战指南:从配置到 simple 模式原理

原创2026-09-20 17:39:121,901 阅读
文章标签:前端Web框架小程序SSR前端构建

基于 @ice/plugin-intl 的 ice.js 国际化(i18n)实战指南:从配置到 simple 模式原理

@ice/plugin-intl 是 ice.js 官方提供的国际化插件,为基于 React 的渐进式应用框架 ice.js 3 提供开箱即用的多语言能力。本文以 packages/plugin-intl/CHANGELOG.md 的版本演进为脉络,结合 packages/plugin-intl/README.md 的完整用法与插件源码,系统讲解如何在 ice.js 项目中接入国际化、如何使用 useIntl / intl 读取消息、如何通过 intlSolution: 'simple' 移除 react-intl 依赖,并深入解析 locale 文件的收集、注入与运行时加载机制。读完本文,你将能独立完成一个 ice.js 应用的多语言改造,并理解其背后的实现原理。

插件定位与版本演进脉络

@ice/plugin-intl 是基于 react-intl 封装的 ice.js 3 插件(见 packages/plugin-intl/package.json 中 "description": "react intl plugin for ice.js 3." 及其 dependencies 中的 react-intl: ^6.0.0)。其能力演进在 packages/plugin-intl/CHANGELOG.md 中清晰可见:

版本 变更内容 对应的实现落点
1.1.0 支持 intl 解决方案的 simple 模式(intlSolution: 'simple') src/index.ts 中 intlSolution 选项与 src/runtime-simple.ts 运行时
1.0.2 兼容 navigator.language 返回带下划线(underslash)的情况 src/intl-until.ts 中 defaultLocale.replace('_', '-')
1.0.1 支持全局获取 locale messages templates/locales.ts.ejs 中挂载 window.__ICE_LOCALE_MESSAGES__ / global.__ICE_LOCALE_MESSAGES__
1.0.0 首个正式版本(Initial release) 插件基础能力:locale 文件收集、运行时 Provider、useIntl / intl 导出

可以看出,插件从“基础可用”逐步走向“轻量可裁剪”,1.0.1 解决了服务端渲染与全局访问场景下消息的获取问题,1.0.2 修复了不同浏览器 navigator.language 返回格式差异(如 zh_CN 而非 zh-CN),1.1.0 则引入了不依赖 react-intl 的 simple 模式,下面逐一展开。

安装与基础配置

安装插件

$ npm i @ice/plugin-intl --save-dev

在 ice.config.mts 中注册插件

在项目根目录的 ice.config.mts 中引入并启用插件:

import { defineConfig } from '@ice/app';
import intl from '@ice/plugin-intl';

export default defineConfig({
  plugins: [
    intl(),
  ],
});

在 src/app.ts 中声明 locale 配置

import { defineAppConfig } from 'ice';
import type { LocaleConfig } from '@ice/plugin-intl/types';

export default defineAppConfig(() => ({}));

export const locale: LocaleConfig = {
  // 自定义 getLocale 方法及其它 react-intl 支持的配置项
  getLocale: () => 'en-US',
};

LocaleConfig 的类型定义在 packages/plugin-intl/src/types.ts:

import type { IntlConfig } from 'react-intl';

interface AdditionalConfig {
  getLocale: () => string;
}

export type LocaleConfig = Partial<IntlConfig> & AdditionalConfig;

也就是说,locale 导出可以配置 getLocale() 方法(用于自定义当前语言环境),同时透传 react-intl 的 IntlConfig 中除 locale、messages 之外的所有配置项。运行时插件在 src/runtime.tsx 中读取该导出:它会从 appExport['locale'] 中解构出 getLocale 与其余配置 l,将 l 连同按当前 locale 取出的 messages 一起传入 createIntl,并用 RawIntlProvider 包裹应用:

const localeConfig: LocaleConfig = (typeof exported === 'function' ? await exported() : exported) || {};
const { getLocale, ...l } = localeConfig;
const locale = getLocale ? getLocale() : getDefaultLocale();

intl = createIntl({
  ...l,
  messages: getLocaleMessages()?.[locale] || {},
  locale: getLocale ? getLocale() : getDefaultLocale(),
}, cache);
addProvider(({ children }) => {
  return <RawIntlProvider value={intl}>{children}</RawIntlProvider>;
});

注意:locale 导出既可以是对象,也可以是返回对象(可为异步)的函数,这为动态计算语言环境(如从 URL、用户配置读取)提供了灵活性。

Locales 目录:消息文件的约定与自动收集

文件约定

所有语言包统一放在 src/locales 目录下,每种语言一个文件,文件名即语言标识。例如 en-US.ts:

export default {
  'app.title': 'My Application',
  'app.welcome': 'Welcome to my application!',
};

插件在构建期通过 fast-glob 以 src/locales/*.{ts,js,json} 的规则扫描语言文件(src/index.ts 中的 globRule),支持 .ts、.js、.json 三种格式。

入口文件的两种处理方式

扫描结果有两种处理分支(src/index.ts 第 51-85 行):

  1. 存在 index.ts|js|json 主入口:直接以 @/locales/index.xxx 作为运行时来源,此时所有语言由用户自行汇总导出。
  2. 不存在主入口:插件调用 renderLocaleEntry 生成一份 locales.ts 模板(templates/locales.ts.ejs),为每个语言文件生成 import 与 export,自动组装成一个 localeMessages 对象:
import en_US from '@/locales/en-US';
// ...其余语言文件

const localeMessages = {
  'en-US': en_US,
  // ...
};

这里有一个细节:ES 模块的 import 说明符中不允许出现 -,因此模板生成代码会把文件名中的 - 替换为 _ 作为局部变量名(const specifier = filename.replace('-', '_')),同时保留原始语言名作为对象的 key。

此外,插件通过 watch.addEvent(/src\/locales/) 监听语言文件的增删,当新增或删除语言文件时自动重新生成 locales.ts,开发期无需手动干预。

全局消息注入(1.0.1 的能力)

生成的 locales.ts.ejs 除了导出 localeMessages,还会把消息挂到全局,这正是 CHANGELOG 1.0.1「support get locale messages for global」的实现:

const LOCALE_MESSAGES_KEY = '__ICE_LOCALE_MESSAGES__';
const DEFAULT_LOCALE_KEY = '__ICE_DEFAULT_LOCALE__';
if (typeof window !== 'undefined') {
  window[LOCALE_MESSAGES_KEY] = window['<%- localeMessagesKey %>'] || localeMessages;
  window[DEFAULT_LOCALE_KEY] = window['<%- defaultLocaleKey %>'];
} else {
  global[LOCALE_MESSAGES_KEY] = localeMessages;
}

浏览器端挂载 window.__ICE_LOCALE_MESSAGES__,非浏览器环境(如 SSR/Node)挂载 global.__ICE_LOCALE_MESSAGES__,从而保证服务端渲染与客户端都能获取到语言消息。localeMessagesKey 与 defaultLocaleKey 是插件可配置项,默认值分别为 __LOCALE_MESSAGES__ 与 __DEFAULT_LOCALE__(用于读取外部注入的兜底值),可见 src/index.ts 顶部 PluginOptions 的定义。

在组件中使用国际化能力

插件默认(intlSolution: 'react-intl')会通过 generator.addExport 将 useIntl 与 intl 从 @ice/plugin-intl/runtime 重新导出到 ice 包(src/index.ts 第 88-98 行),因此业务代码直接 import { useIntl } from 'ice' 即可。

使用 useIntl 钩子

import { useIntl } from 'ice';

export default function Home() {
  const intl = useIntl();
  console.log(intl.formatMessage({ id: 'new' }));
  return <h1>home</h1>;
}

useIntl 来自 react-intl(src/runtime.tsx 中 import { ... useIntl } from 'react-intl'),配合 RawIntlProvider 使用,适合在组件内响应式地获取当前语言与格式化函数。

在非组件环境使用 intl 实例

import { intl } from 'ice';

function alertMessage() {
  alert(intl.formatMessage({ id: 'app.welcome' }));
}

模块顶层导出的 intl 是 IntlShape 实例(同样来自 src/runtime.tsx),可在事件回调、工具函数等非 React 组件场景直接调用 formatMessage、formatDate 等 react-intl 全部能力。

Simple 模式:移除 react-intl 依赖

CHANGELOG 1.1.0 的核心变更,是提供了一种「不依赖 react-intl」的轻量方案。当应用只需要简单的消息替换时,可以通过 intlSolution: 'simple' 裁剪依赖,减小打包体积。

开启方式

import { defineConfig } from '@ice/app';
import intl from '@ice/plugin-intl';

export default defineConfig({
  plugins: [
    // intlSolution: 'simple' 可移除 react-intl 依赖
    intl({ intlSolution: 'simple' }),
  ],
});

从 src/index.ts 可以看到,intlSolution 默认为 'react-intl',可选值为 'react-intl' | 'simple'。当设为 'simple' 时,插件导出的是 @ice/plugin-intl/runtime-simple,runtime 字段也随之切换为 @ice/plugin-intl/runtime-simple,并不再导出 useIntl(只有 intl),因此项目中无法再使用 hook 形式。

Simple 模式的使用方式

Simple 模式下的 intl.formatMessage 是受限实现(src/runtime-simple.ts),仅支持两种调用签名:

import { intl } from 'ice';

export default function Home() {
  console.log(intl.formatMessage({ id: 'app.welcome', defaultMessage: 'Welcome to my application!' }));
  console.log(intl.formatMessage('app.welcome'));
  return <h1>home</h1>;
}

带变量的用法:

intl.formatMessage({ id: 'app.welcome' }, { name: 'icejs' });

注意事项:simple 模式的消息语法只支持 {name} 这类占位符替换,不支持 react-intl 完整的 ICU MessageFormat 语法(如复数、选择器、富文本等)。同时,runtime-simple 中如果检测到 locale 导出里存在除 getLocale 之外的其它配置,会在控制台输出警告「The locale config is not supported in the simple mode.」。

Simple 模式的实现原理

查看 src/runtime-simple.ts,其核心是一个手写的轻量 intl 对象:

const intl = {
  formatMessage: (message: MessageDescriptor | string, values?: Record<string, string>) => {
    let messageId = typeof message === 'string' ? message : message.id;
    const defaultMessage = typeof message === 'string' ? message : message.defaultMessage;
    let content = localeMessages?.[currentLocale]?.[messageId];
    if (!content) {
      console.error(`Message with id "${messageId}" not found in locale "${currentLocale}"`);
      return defaultMessage || messageId;
    }
    if (values) {
      Object.keys(values).forEach((key) => {
        content = content.replace(new RegExp(`{${key}}`, 'g'), values[key]);
      });
    }
    return content;
  },
};

逻辑非常直白:按 当前 locale -> 消息 id 两级索引查找文案;找不到时回退到 defaultMessage,再不行直接返回消息 id 并在控制台报错;有变量时用正则 {key} 全局替换。运行时会在启动阶段读取 appExport['locale'] 中的 getLocale 并缓存当前 locale,同时因为 import 提升(import hoisting)的存在,会在运行时重新刷新一次 localeMessages 以保证 SSR 场景下消息已就绪(源码注释「Refresh locale messages for server side because of import hoisting.」)。

语言环境探测与下划线兼容(1.0.2 的修复)

无论哪种模式,默认语言环境的探测逻辑都集中在 src/intl-until.ts:

export const getDefaultLocale = () => {
  const defaultLocale = (typeof window !== 'undefined' && window.__ICE_DEFAULT_LOCALE__) ||
    (typeof navigator !== 'undefined' && navigator.language) ||
    'zh-CN';
  return defaultLocale.replace('_', '-');
};

探测优先级为:window.__ICE_DEFAULT_LOCALE__(外部注入的默认语言)→ navigator.language(浏览器语言)→ 兜底 'zh-CN'。而结尾的 defaultLocale.replace('_', '-') 正是 CHANGELOG 1.0.2 的修复点:部分环境(例如某些浏览器或测试环境)的 navigator.language 会返回 zh_CN 这样的下划线格式,与语言文件名约定(zh-CN.ts)不一致,统一替换为连字符后即可正确命中对应语言包。

完整示例:examples/with-intl

仓库中的 examples/with-intl 示例项目提供了可直接运行的最小实践:

对照该示例阅读 packages/plugin-intl 目录下的源码(src/index.ts、src/runtime.tsx、src/runtime-simple.ts、src/intl-until.ts、templates/locales.ts.ejs),可以完整还原「语言文件扫描 → locales.ts 生成 → 运行时注入 Provider → 业务调用」的整条链路。

小结:两个模式如何选

对比维度 react-intl 模式(默认) simple 模式
依赖 引入 react-intl 移除 react-intl,打包更轻
API useIntl hook + intl 实例 仅 intl.formatMessage
语法能力 完整 ICU MessageFormat 仅 {name} 占位符替换
locale 配置 支持 IntlConfig 全部配置 仅支持 getLocale,其它配置会告警
适用场景 复杂格式化、日期/数字、组件内响应式切换 简单文案替换、追求最小体积

从 CHANGELOG 的版本演进可以观察到,@ice/plugin-intl 在保持 react-intl 完整能力的同时,为「只需要简单文案替换」的场景提供了可裁剪的 simple 模式。实际选型时,若应用依赖 ICU 语法或需要在组件内随语言切换响应式渲染,请使用默认的 react-intl 模式;若仅需静态文案映射并希望减小体积,则可选用 intlSolution: 'simple'。

登录后查看全文
ice