写点什么

数栈产品如何实现国际化

作者:袋鼠云数栈
  • 2025-09-23
    浙江
  • 本文字数:7076 字

    阅读完需:约 23 分钟

数栈产品如何实现国际化

在全球化的浪潮下,越来越多的企业需要支持多语言、多区域的数据平台工具。数栈产品作为一款成熟的数据开发与治理平台,早已实现系统级的国际化(i18n)支持。数栈产品团队深入挖掘多语言使用场景,打造了一套完整的国际化解决方案,让产品真正实现无障碍全球使用。本文将详细介绍数栈产品在国际化方面的实现机制与使用方式,帮助用户更好地理解和应用该功能。

一、什么是国际化?为什么如此重要?

国际化(Internationalization,简称 i18n)是指软件开发过程中,将产品与特定语言及地区脱钩的过程,以便产品能够轻松适应不同语言和地区需求。当产品需要进入全球市场时,国际化能够大幅降低本地化成本,提升用户体验。

数栈产品的国际化工作涵盖了界面文本、日期时间、货币格式、数字格式等多个方面,确保无论用户来自哪个国家地区,都能获得自然流畅的使用体验。

二、国际化实现整体思路

数栈产品的国际化实现主要分为两个核心阶段:开发阶段的静态文本提取和运行时的动态文本渲染。通过这两个阶段的配合,我们实现了产品界面的中英文无缝切换。

第一阶段:开发阶段 - 中文文本提取与替换

在开发过程中,我们使用专门的中文提取工具,将产品界面中的所有中文文本进行统一提取和标准化处理:

这个阶段的关键在于:

●识别代码中的所有中文文本

●生成统一的国际化键值对(如 I18N.XXX)

●保持代码的可读性和可维护性

第二阶段:运行时 - 中英文文本动态渲染

在产品运行期间,我们通过 dt-intl 工具实现多语言文本的动态渲染:

运行机制:

1.初始化检测:页面加载时读取当前语言配置(中文或英文)

2.语言包加载:根据配置加载对应的语言资源文件

3.实时渲染:将 I18N.XXX 占位符替换为实际的目标语言文本

三、如何实现国际化?

接下来我们详细介绍实现国际化的具体操作。

(一)确定产品中文类型

先需要明确项目中可能存在中文的情况有哪些?

虽然有很多情况下会出现中文,在代码中存在的时候大部分是 string 或者模版字符串,在 react 中的时候一个是 dom 的子节点还是一种是 prop 上的属性包含中文。

const a = '霜序';const b = `霜序`;const c = `${isBoolean} ? "霜序" : "FBB"`;const obj = { a: '霜序' };enum Status {    Todo = "未完成",    Complete = "完成"}// enum Status {//     "未完成",//     "完成"// }const dom = <div>霜序</div>;const dom1 = <Customer name="霜序" />;
复制代码

​类型一:StringLiteral

// const a = '霜序';
{ "type": "StringLiteral", "start": 10, "end": 14, "extra": { "rawValue": "霜序", "raw": "'霜序'" }, "value": "霜序}
复制代码

对应的 AST 节点为 StringLiteral,需要去遍历所有的 StringLiteral 节点,将当前的节点替换为我们需要的 I18N.key 这种节点。

类型二:TemplateLiteral

// const b = `${finalRoles}(质量项目:${projects})`{    "type": "TemplateLiteral",    "start": 10,    "end": 43,    "expressions": [        {            "type": "Identifier",            "start": 13,            "end": 23,            "name": "finalRoles"        },        {            "type": "Identifier",            "start": 32,            "end": 40,            "name": "projects"        }    ],    "quasis": [        {            "type": "TemplateElement",            "start": 11,            "end": 11,            "value": {                "raw": "",                "cooked": ""            }        },        {            "type": "TemplateElement",            "start": 24,            "end": 30,            "value": {                "raw": "(质量项目:",                "cooked": "(质量项目:"            }        },        {            "type": "TemplateElement",            "start": 41,            "end": 42,            "value": {                "raw": ")",                "cooked": ")"            }        }    ]}
复制代码

相对于字符串情况会复杂一些,TemplateLiteral 中会出现变量的情况,能够看到在 TemplateLiteral 节点中存在 expressions 和 quasis 两个字段分别表示变量和字符串。

其实可以发现对于字符串来说全部都在 TemplateElement 节点上,那么是否可以直接遍历所有的 TemplateElement 节点,和 StringLiteral 一样。

直接遍历 TemplateElement 的时候,处理之后的效果如下:

const b = `${finalRoles}(质量项目:${projects})`
const b = `${finalRoles}${I18N.K}${projects})`
// I18N.K = "(质量项目:"
复制代码

​那么这种只提取中文不管变量的情况,会导致翻译不到的问题,上下文很缺失。

最后我们会处理成{val1}(质量项目:{val2})的情况,将对应 val1 和 val2 传入:

I18N.get(I18N.K, {    val1: finalRoles,    val2: projects,})
复制代码

​类型三:JSXText

{    "type": "JSXElement",    "start": 12,    "end": 25,    "children": [        {            "type": "JSXText",            "start": 17,            "end": 19,            "extra": {                "rawValue": "霜序",                "raw": "霜序"            },            "value": "霜序"        }    ]}
复制代码

对应的 AST 节点为 JSXText,去遍历 JSXElement 节点,在遍历对应的 children 中的 JSXText 处理中文文本。

类型四:JSXAttribute

{    "type": "JSXOpeningElement",    "start": 13,    "end": 35,    "name": {        "type": "JSXIdentifier",        "start": 14,        "end": 22,        "name": "Customer"    },    "attributes": [        {            "type": "JSXAttribute",            "start": 23,            "end": 32,            "name": {                "type": "JSXIdentifier",                "start": 23,                "end": 27,                "name": "name"            },            "value": {                "type": "StringLiteral",                "start": 28,                "end": 32,                "extra": {                    "rawValue": "霜序",                    "raw": "\"霜序\""                },                "value": "霜序"            }        }    ],    "selfClosing": true}
复制代码

​对应的 AST 节点为 JSXAttribute,中文存在的节点还是 StringLiteral,但是在处理的时候还是特殊处理 JSXAttribute 中的 StringLiteral,因为对于这种 JSX 中的数据来说我们需要包裹{},不是直接做文本替换的。

(二)用工具(Babel)转换中文

使用 Babel 做转换,其核心逻辑如下图:

步骤一:转化源码

使用 @babel/parser 将源代码转译为 AST

const plugins: ParserOptions['plugins'] = [    'decorators-legacy',    'typescript',];if (fileName.endsWith('jsx') || fileName.endsWith('tsx')) {    plugins.push('jsx');}const ast = parse(sourceCode, {    sourceType: 'module',    plugins,});
复制代码

​步骤二:处理 AST

(1)@babel/traverse 特殊处理上述的节点,转化 AST

babelTraverse(ast, {    StringLiteral(path) {        const { node } = path;        const { value } = node;        if (            !value.match(DOUBLE_BYTE_REGEX) ||            (path.parentPath.node.type === 'CallExpression' &&                path.parentPath.toString().includes('console'))        ) {            return;        }        path.replaceWithMultiple(template.ast(`I18N.${key}`));    },    TemplateLiteral(path) {        const { node } = path;        const { start, end } = node;        if (!start || !end) return;        let templateContent = sourceCode.slice(start + 1, end - 1);        if (            !templateContent.match(DOUBLE_BYTE_REGEX) ||            (path.parentPath.node.type === 'CallExpression' &&                path.parentPath.toString().includes('console')) ||            path.parentPath.node.type === 'TaggedTemplateExpression'        ) {            return;        }        if (!node.expressions.length) {            path.replaceWithMultiple(template.ast(`I18N.${key}`));            path.skip();            return;        }        const expressions = node.expressions.map((expression) => {            const { start, end } = expression;            if (!start || !end) return;            return sourceCode.slice(start, end);        });        const kvPair = expressions.map((expression, index) => {            templateContent = templateContent.replace(                `\${${expression}}`,                `{val${index + 1}}`,            );            return `val${index + 1}: ${expression}`;        });        path.replaceWithMultiple(            template.ast(                `I18N.get(I18N.${key},{${kvPair.join(',\n')}})`,            ),        );    },    JSXElement(path) {        const children = path.node.children;        const newChild = children.map((child) => {            if (babelTypes.isJSXText(child)) {                const { value } = child;                if (value.match(DOUBLE_BYTE_REGEX)) {                    const newExpression = babelTypes.jsxExpressionContainer(                        babelTypes.identifier(`I18N.${key}`),                    );                    return newExpression;                }            }            return child;        });        path.node.children = newChild;    },    JSXAttribute(path) {        const { node } = path;        if (            babelTypes.isStringLiteral(node.value) &&            node.value.value.match(DOUBLE_BYTE_REGEX)        ) {            const expression = babelTypes.jsxExpressionContainer(                babelTypes.memberExpression(                    babelTypes.identifier('I18N'),                    babelTypes.identifier(`${key}`),                ),            );            node.value = expression;        }    },});
复制代码

对于 TemplateLiteral 来说需要处理 expression,通过截取的方式获取到对应的模版字符串 templateContent:

●如果不存在 expressions,直接类似 StringLiteral 处理

●如果存在 expressions,遍历 expressions 通过 ${val(index)}替换掉 templateContent 中的 expression,最后使用 I18N.get 的方式获取对应的值

const name = `${a}霜序`;// const name = I18N.get(I18N.test.A, { val1: a });
const name1 = `${a ? "霜" : "序"}霜序`;// const name1 = I18N.get(I18N.test.B, { val1: a ? I18N.test.C : I18N.test.D });
复制代码

​对于 TemplateLiteral 节点来说,如果是嵌套的情况,会出现问题。

const name1 = `${a ? `霜` : `序`}霜序`;// const name1 = I18N.get(I18N.test.B, { val1: a ? `霜` : `序` });
复制代码

​🤔 为何对于 TemplateLiteral 中嵌套的 StringLiteral 会处理,而 TemplateLiteral 就不处理呢? 

💡 原因是 babel 不会自动递归处理 TemplateLiteral 的子级嵌套模板。

(2)上述的代码中通过遍历一些 AST 处理完了之后,我们需要统一引入当前 I18N 这个变量。

我们需要在当前文件的 AST 顶部的 import 语句后插入当前的 importStatement:

Program: {    exit(path) {        const importStatement = projectConfig.importStatement;        const result = importStatement            .replace(/^import\s+|\s+from\s+/g, ',')            .split(',')            .filter(Boolean);        // 判断当前的文件中是否存在 importStatement 语句        const existingImport = path.node.body.find((node) => {            return (                babelTypes.isImportDeclaration(node) &&                node.source.value === result[1]            );        });        if (!existingImport) {            const importDeclaration = babelTypes.importDeclaration(                [                    babelTypes.importDefaultSpecifier(                        babelTypes.identifier(result[0]),                    ),                ],                babelTypes.stringLiteral(result[1]),            );            path.node.body.unshift(importDeclaration);        }    },}
复制代码

​步骤三:转回代码

const { code } = generate(ast, {    retainLines: true,    comments: true,});
复制代码

处理完之后的代码如下图:

(三)读取国际化 key

开发 dt-intl 仓库,主要是为了做通过 I18N.xxx/I18N.get(I18N.xxx)到对应文本

dt-intl 默认导出一个方法,仅支持 init 方法

import dtIntl from 'dt-intl';
const I18N = dtIntl.init<I18NType>(currentLang, langs, LangEnum.zhCN);
复制代码

​返回的 I18N 是一个响应式对象,通过 Object.defineProperty/Proxy 实现属性的响应式变化

const defineReactive = (obj, key, defaultKey) => {    let childObj = observe(obj[key]);    Object.defineProperty(obj, key, {        get() {            if (obj.__data__[key]) {                return getProxyObj(obj.__data__[key]);            } else if (obj.__metas__[defaultKey][key]) {                return getProxyObj(obj.__metas__[defaultKey][key]);            } else {                return getDefaultProxyString();            }        },        set(newVal) {            if (obj[key] === newVal) {                return;            }            // 如果值有变化的话,做一些操作            obj[key] = newVal;            // 执行回调            const cb = obj.callback[key];            cb.call(obj);            // 如果set进来的值为复杂类型,再递归它,加上set/get            childObj = observe(newVal);        },    });};
复制代码

可以直接通过 I18N.xxx 获取到对应的文本

还提供 template/get 的方式处理带有参数的文案,template 处理简单的模版字符串,get 支持 IntlMessageFormat 处理复杂的模版字符串:

template(str, args) {    if (!str) {        return '';    }    return str.replace(/\{(.+?)\}/g, (match, p1) => {        return this.getProp({ ...this.__data__, ...args }, p1);    });}
get(str, args?) { let msg = lodashGet(this.__data__, str); if (!msg) { msg = lodashGet(this.__metas__[this.__defaultKey__ || 'zh-CN'], str, str); } if (args) { msg = new IntlMessageFormat(msg, this.__lang__); msg = msg.format(args); return msg; } else { return msg; }}
复制代码

​(四)最后接入国际化

import { getCurrentLang, LangEnum } from 'xxx/src/utils/i18n';import dtIntl from 'dt-intl';
// 所提出来的中文文本import zhCNData from '../locales/zh-CN/index';
export type I18NType = typeof zhCNData;
// 当前的语言const currentLang = getCurrentLang() || LangEnum.zhCN;
const langs = { [currentLang]: require(`../locales/${currentLang}/index.ts`).default, [LangEnum.zhCN]: zhCNData,};
const I18N = dtIntl.init<I18NType>(currentLang, langs);
export default I18N;
复制代码

​提供了 changeLanguage 统一处理切换语言的情况:

export const changeLanguage = (lang: LangType) => {    localStorage.setItem(LOCALE_KEY, lang);    const hash = window.location.hash;    const [path, queryString] = hash.split('?');    if (!queryString) {        window.location.reload();        return;    }    const params = new URLSearchParams(queryString);    params.delete('lang');    const size = Array.from(params).length;    window.location.hash = `${path}${size ? `?${params.toString()}` : ''}`;    window.location.reload();};
复制代码

​最后实现效果如下:

四、结语

国际化不是简单的文本翻译,而是一个系统工程,需要从前端到后端全面考虑。数栈产品通过完善的国际化架构和实践经验,为全球用户提供了无缝的使用体验。

在全球化浪潮下,提前布局国际化能力,将为产品的国际市场拓展奠定坚实基础。希望数栈产品的国际化实践能为您的项目提供有益参考!


以上就是数栈产品如何实现国际化的所有内容~

未来,数栈还将持续迭代国际化能力,根据不同区域用户的反馈优化交互体验,让技术真正 “无国界”。如果你在使用数栈国际化版本时遇到问题,或有新的需求建议,欢迎在评论区留言,我们一起让数据工具更懂世界,也让世界更易用好数据~

最后,别忘了把数栈的国际化实践分享给更多需要的朋友,一起见证数据技术的全球生长!

用户头像

还未添加个人签名 2021-05-06 加入

还未添加个人简介

评论

发布
暂无评论
数栈产品如何实现国际化_国际化_袋鼠云数栈_InfoQ写作社区