数栈产品如何实现国际化
在全球化的浪潮下,越来越多的企业需要支持多语言、多区域的数据平台工具。数栈产品作为一款成熟的数据开发与治理平台,早已实现系统级的国际化(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();
};
最后实现效果如下:
 
四、结语
国际化不是简单的文本翻译,而是一个系统工程,需要从前端到后端全面考虑。数栈产品通过完善的国际化架构和实践经验,为全球用户提供了无缝的使用体验。
在全球化浪潮下,提前布局国际化能力,将为产品的国际市场拓展奠定坚实基础。希望数栈产品的国际化实践能为您的项目提供有益参考!
以上就是数栈产品如何实现国际化的所有内容~
未来,数栈还将持续迭代国际化能力,根据不同区域用户的反馈优化交互体验,让技术真正 “无国界”。如果你在使用数栈国际化版本时遇到问题,或有新的需求建议,欢迎在评论区留言,我们一起让数据工具更懂世界,也让世界更易用好数据~
最后,别忘了把数栈的国际化实践分享给更多需要的朋友,一起见证数据技术的全球生长!
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号