基于 @ice/plugin-intl 的 ice.js 国际化(i18n)实战指南:从配置到 simple 模式原理
基于 @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 行):
- 存在
index.ts|js|json主入口:直接以@/locales/index.xxx作为运行时来源,此时所有语言由用户自行汇总导出。 - 不存在主入口:插件调用
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 示例项目提供了可直接运行的最小实践:
- ice.config.mts:注册
intl()插件; - src/locales/zh-CN.json 与
en-US.json:两个语言的 JSON 消息文件(JSON 格式同样受支持); - src/locales/index.ts:语言文件入口,汇总导出各语言消息;
- src/pages/index.tsx:在组件中使用
useIntl读取消息并切换语言的页面。
对照该示例阅读 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'。