Webpack i18n plugin
🌍 webpack-i18n-assets-plugin
一个 Webpack 插件,用于将本地化函数 (i18n) 的调用替换为目标文本。
特性
- 将 i18n 文本内联到 bundle 中(同时将参数替换到最终字符串中)
- 在一次构建中为所有 locale 生成资源
- 该插件仅适用于生产构建!
- 只支持本地化函数参数中的字面量作为键(不允许使用模板字符串和变量)
📝 如何使用
-
安装包:
npm i -D @gravity-ui/webpack-i18n-assets-plugin -
将插件连接到 Webpack(以
@gravity-ui/app-builder为例):Webpack 配置示例 (
webpack.config.js):const {I18nAssetsPlugin} = require('@gravity-ui/webpack-i18n-assets-plugin'); // 示例。读取所有包含本地化文本的文件并存储在此映射中 const locales = { en: {}, ru: {}, tr: {}, }; module.exports = { output: { filename: '[name].[locale].js', // 文件名中需要 [locale] }, plugins: [ new I18nAssetsPlugin({ locales }) ] }如果您想为每个 locale 创建资源清单(assets manifests)的示例 (
webpack.config.js):const {applyPluginToWebpackConfig} = require('@gravity-ui/webpack-i18n-assets-plugin'); const locales = { en: {}, ru: {}, tr: {}, }; // 一些现有的 webpack 配置 const webpackConfig = { plugins: [ ... ], ... }; // 使用 applyPluginToWebpackConfig 时,还会连接 WebpackAssetsManifest 插件, // 该插件将为每个 locale 生成资源清单。 module.exports = applyPluginToWebpackConfig(webpackConfig, {locales});如果您使用
@gravity-ui/app-builder的示例:import type {ServiceConfig} from '@gravity-ui/app-builder'; import {applyPluginToWebpackConfig, Options} from '@gravity-ui/webpack-i18n-assets-plugin'; const locales = { en: {}, ru: {}, tr: {}, }; // 使用 applyPluginToWebpackConfig 时,还会连接 WebpackAssetsManifest 插件, // 该插件将为每个 locale 生成资源清单。 const config: ServiceConfig = { client: { webpack: (originalConfig) => applyPluginToWebpackConfig(originalConfig, {locales}), }, } -
在服务器上配置动态静态资源(使用
@gravity-ui/app-layout的示例):import {createRenderFunction, createLayoutPlugin} from '@gravity-ui/app-layout'; const renderLayout = createRenderFunction([ createLayoutPlugin({ manifest: ({lang = 'en'}) => { return `assets-manifest.${lang}.json`; }, publicPath: '/build/', }), ]); app.get((req, res) => { res.send( renderLayout({ title: 'Home page', pluginsOptions: { layout: { name: 'home', }, }, }), ); });
🔧 设置
默认情况下,该插件配置为与 @gravity-ui/i18n 库一起工作,但您可以为任何其他 i18n 库自定义处理方式。
importResolver
处理导入并标记哪些导入应被视为本地化函数的函数(之后,对标记标识符的调用由 replacer 处理)。
签名与 webpack 的原始 importSpecifier 类似。
示例:
const importResolver = (source: string, exportName: string, _identifierName: string, module: string) => {
// 如果需要忽略基于特定路径的模块处理,可以这样处理。
if (module.startsWith('src/units/compute')) {
return undefined;
}
// 处理全局函数的默认导入
// import i18n from 'ui/utils/i18n'
if (source === 'ui/utils/i18n' && exportName === 'default') {
return {
resolved: true,
keyset: undefined,
};
}
// 处理辅助函数的导入,并指定它属于公共 keyset(命名空间)。
// import {ci18n} from 'ui/utils/i18n'
if (source === 'ui/utils/i18n' && exportName === 'ci18n') {
return {
resolved: true,
keyset: 'common',
};
}
return undefined;
};
declarationResolver
处理变量声明并标记哪些变量应被视为本地化函数的函数(之后,对标记标识符的调用由 replacer 函数处理)。
示例:
import type {VariableDeclarator} from 'estree';
const declarationResolver = (declarator: VariableDeclarator, module: string) => {
// 如果需要忽略基于特定路径的模块处理,可以这样处理。
if (module.startsWith('src/units/compute')) {
return undefined;
}
// 处理函数声明,例如 const i18nK = i18n.bind(null, 'keyset');
if (
declarator.id.type === 'Identifier' &&
declarator.id.name.startsWith('i18n') &&
declarator.init &&
isI18nBind(declarator.init)
) {
return {
functionName: declarator.id.name,
keyset: getKeysetFromBind(declarator.init),
};
}
return undefined;
};
replacer
类型:Replacer
一个处理本地化函数调用并返回字符串替换的函数。
示例:
import type {VariableDeclarator} from 'estree';
import type {ReplacerArgs, ReplacerContext} from '@gravity-ui/webpack-i18n-assets-plugin';
function replacer(
this: ReplacerContext,
{callNode, key: parsedKey, keyset: parsedKeyset, localeName}: ReplacerArgs,
) => {
let key = parsedKey;
let keyset = parsedKeyset;
let params: Expression | SpreadElement | undefined;
const getStringValue = (node: Expression | SpreadElement) => {
if (node.type === 'Literal' && typeof node.value === 'string') {
return node.value;
}
throw new Error('Incorrect argument type in localizer call');
};
// 处理带有一个参数的调用 i18nK('key')
if (callNode.arguments.length === 1) {
key = getStringValue(callNode.arguments[0]);
} else if (callNode.arguments.length === 2) {
// 处理 i18n('keyset', 'key') 或 i18nK('key', {params})
const [firstArg, secondArg] = callNode.arguments;
// 调用 i18n('keyset', 'key')
if (secondArg.type === 'Literal') {
keyset = getStringValue(firstArg);
key = getStringValue(secondArg);
} else {
// 调用 i18nK('key', {params})
key = getStringValue(firstArg);
params = secondArg;
}
} else if (callNode.arguments.length === 3) {
// 调用 i18n(namespace, key, params)
const [firstArg, secondArg, thirdArg] = callNode.arguments;
keyset = getStringValue(firstArg);
key = getStringValue(secondArg);
params = thirdArg;
} else {
throw new Error('Incorrect count of arguments in localizer call');
}
// 务必处理从函数调用参数中获取的 key。
// 如果函数与 keyset 相关,修改代码后,keyset 可以插入到 key 中(这是插件的功能)。
// 如果使用来自 ReplacerArgs 的 key,它不带 keyset,不需要处理。
const keyParts = key.split('::');
if (keyParts.length === 2) {
key = keyParts[1];
}
const value = this.resolveKey(key, keyset);
// 在此处根据您的需求实现替换选项。
// 例如,如果 key 是复数形式,则返回一个函数调用等。
return JSON.stringify(value);
};
collectUnusedKeys
类型:[Boolean] (默认 - false)
启用收集项目中未使用的 key 的模式。构建后,它会创建一个名为 unused-keys.json 的文件。
为确保功能正常,在 Replacer 函数中始终返回详细格式是必要的。这一点很重要,因为在替换过程中,有可能修改自动确定的 key 和 keyset。
Frameworks settings
Gravity i18n
用于处理来自 @gravity-ui/i18n 库的本地化函数调用的函数。
现成的函数位于 此处。
函数将处理的代码示例:
// importResolver 只考虑 ui/utils/i18n 路径上的默认导入。
import i18n from 'ui/utils/i18n';
// declarationResolver 处理值为 i18n.bind 调用的变量。
const i18nK = i18n.bind(null, 'component.navigation');
// replacer 处理由 importResolver 和 declarationResolver 找到的标识符的调用
// 这意味着以下调用将被处理:
i18nK('some_key');
i18nK('some_plural_key', { count: 123 });
i18nK('some_key_with_param', { someParam: 'hello' });
i18n('component.navigation', 'some_key');
i18n('component.navigation', 'some_plural_key', { count: 123 });
i18n('component.navigation', 'some_key_with_param', { someParam: 'hello' });
Replacer 额外执行以下操作:
-
将参数内联到字符串中。例如,如果 key 的值为:
const keyset = { some_key: 'string value with {{param}}' }; i18nK('some_key', {param: getSomeParam()}) // 替换后,我们将得到: `string value with ${getSomeParam()}` -
为复数 key 替换自调用函数:
const keyset = { pural_key: [ 'one_form {{count}}', 'few_form {{count}}', 'many_form {{count}}', 'other_form {{count}}', ], }; i18nK('pural_key', {count: getSomeCount()}) // 替换后,我们将得到: (function(f,c){ const v=f[!c ? "zero" : new Intl.PluralRules("${locale}").select(c)]; return v && v.replaceAll("{{count}}",c); })({ "one": "one_form {{count}}", "few": "few_form {{count}}", "many": "many_form {{count}}", "other": "other_form {{count}}" }, getSomeCount())
ℹ️ FAQ
与 webpack-localize-assets-plugin 相比如何?
本插件的实现借鉴了 webpack-localize-assets-plugins 包中的一些想法(非常感谢该包的创建者!)。
区别如下:
- 更便捷的 API,允许您使用任何类型的国际化函数(包括命名空间助手,如 i18next 的
useTranslation,或其他模块导入的函数等)。 - 正确生成相对于源代码的源映射。
- 仅支持 webpack 5。已移除对 webpack 4 的支持。