1. 天舟云前端国际化架构设计

底层国际化实施分为两个部分:

  • 自定义国际化:react-intl
  • 表单国际化:antd/config-provider

1.1 现支持的国际化语言包键名映射

须在接入国际化的系统中,增加所有语言区域对应的语言包文件

键名 语言/地区 语言包文件名 语言展示文案
zh-CN 中文(中国大陆) zh_CN.js 简体中文
en-US 英语(美国) en_US.js English(US)

如果业务域想要自定义「键名」、「语言包文件名」、「语言展示文案」,可参考下文「5.5 自定义国际化」

2. 天舟云集成

2.1 Apollo配置

tianzhou.env中增加如下配置:

i18n.language.key = 'zh-CN'    # 默认国际化语言包的键名名称,默认为zh-CN
i18n.language.keys = 'zh-CN,en-US'    # 默认国际化语言包键名集合字符串,默认为'zh-CN,en-US'
i18n.language.fileNames = 'zh_CN,en_US'    # 默认国际化语言包文件名集合字符串,默认为'zh_CN,en_US'
i18n.language.labels = '简体中文,English(US)'    # 默认国际化语言包文件名展示文本集合字符串,默认为'简体中文,English(US)'

2.2 config/server.js修改

config/server.js中做如下修改:

module.exports = {
    // 其他代码
    injectData: {
        // 其他代码
        // 2023.05.23 设置默认国际化语言包的键名名称,默认为zh-CN
        { key: 'i18nLanguageKey', value: process.env['i18n.language.key'] || 'zh-CN' },
        // 2023.06.29 增加国际化语言包键名集合字符串,默认为'zh-CN,en-US'
        { key: 'i18nLanguageKeys', value: process.env['i18n.language.keys'] || ['zh-CN', 'en-US'].join(',') },
        // 2023.06.29 增加国际化语言包文件名集合字符串,默认为'zh_CN,en_US'
        { key: 'i18nLanguageFileNames', value: process.env['i18n.language.fileNames'] || ['zh_CN', 'en_US'].join(',') },
        // 2023.06.29 增加国际化语言包文件名展示文本集合字符串,默认为'简体中文,English(US)'
        // 需要注意语言包键名及文件名的集合字符串需要一一对应,否则会映射错误
        {
            key: 'i18nLanguageLabels',
            value: process.env['i18n.language.labels'] || ['简体中文', 'English(US)'].join(','),
        },
    }
}

2.3 开启顶部工具栏切换语言

config/client.js中做如下修改:

// 其他代码
plugins: [
    {
        name: 'AntdFrame',
        config: {
            showI18nSelect: true,
            // 其他代码
        }
        // 其他代码
    }
    // 其他代码
]
// 其他代码

3. 业务系统集成

3.1 npm包升级

3.1.1 CSB系统

如果是CSB系统体系,则升级如下版本:

npm install @cvte/cir-framework@4.0.12 @cvte/cir-login@3.0.2 @cvte/cir-antdframe@5.0.4 @cvte/cir-lcp-sdk@1
# 或使用yarn
yarn add @cvte/cir-framework@4.0.12 @cvte/cir-login@3.0.2 @cvte/cir-antdframe@5.0.4 @cvte/cir-lcp-sdk@1

3.1.2 非CSB系统

如果非CSB系统体系,则增加@cvte/cir-lcp-sdk@1.3.3及之后版本(<2.0.0的版本)依赖:

@cvte/cir-lcp-sdk@1.3.3 及之后版本中内置了 react-intl基础包(版本:3.12.1

npm install @cvte/cir-lcp-sdk@1
# 或使用yarn
yarn add @cvte/cir-lcp-sdk@1

3.2 Apollo配置

参考上文「天舟云集成」>「Apollo配置

3.3 config/server.js修改

参考上文「天舟云集成」>「config/server.js修改

3.4 资源中心依赖共享

webpack5增加共享

'react-intl': {
          eager: true,
          singleton: true,
        }

3.5 国际化文件

❗️❗️❗️ 须在接入国际化的系统中,增加所有语言区域对应的语言包文件

3.5.1 CSB系统

在目录:/app/modules/App/locales下放置语言包文件

在目录:/app/modules/App/locales下新建一个index.ts,该文件的作用是自定义国际化初始化逻辑,可查看下文的「5.5 自定义国际化」,如无需要自定义,则建立空白内容文件即可

文件名请查看上文的「现支持的国际化语言包键名映射」中的语言包文件名

文件内容请查看下文的「国际化语言包内容

3.5.2 非CSB系统

在自定目录下放置语言包文件

文件名请查看上文的「现支持的国际化语言包键名映射」中的语言包文件名

文件内容请查看下文的「国际化语言包内容

3.5.3 国际化语言包内容

使用@cvte/cir-lcp-sdk进行引入语言包文件,要求版本需要>=1.3.1,当前最新版本须查看下文的「支持国际化资源版本列表

zh_CN.js示例:

import zhCn from '@cvte/cir-lcp-sdk/src/lib/locales/zh_CN';
export default zhCn;
// 如果业务系统需要额外补充国际化内容可采用如下方式:
const customIntl = {
    ...zhCn||{},
    'A>src.a.index>render.div.label': '第{number}页'
}
export default customIntl;

3.6 组件接入国际化

语言框架仅支持react

3.6.1 准备工作

安装@cvte/cir-lcp-sdk最新稳定版本

npm i @cvte/cir-lcp-sdk
# 或
yarn add @cvte/cir-lcp-sdk

需要确保支持国际化的组件外层须有<IntlProvider></IntlProvider>包裹

示例如下:

// 假设需要支持国际化的组件为DemoComponent

// @cvte/cir-lcp-sdk版本>=1.3.10的接入方式
import { IntlWrapper } from '@cvte/cir-lcp-sdk';
import { DemoComponent } from './DemoComponent';

export default IntlWrapper(DemoComponent, { forwardRef: true });

// @cvte/cir-lcp-sdk版本<1.3.10的接入方式
import React from 'react';
import { IntlProvider } from 'react-intl';
import { i18nLanguageMemoName } from '@cvte/cir-lcp-sdk';
import { II18nLocalData } from '@cvte/cir-lcp-sdk/src/interface/i18n';
import CirCacheBase from '@cvte/cir-cache';
import { DemoComponent } from './DemoComponent';

const IntlWrapper = (props) => {
    const cirCache = new CirCacheBase({ storage: 'memory' });
    // 增加获取国际化缓存数据
    const { locale, customLocale: messages } = cirCache.getValue<II18nLocalData>(i18nLanguageMemoName) || {};

    return (
    <IntlProvider
        {...{
            locale,
            messages,
        }}
    >
        <DemoComponent {...props||{}} />
    </IntlProvider>
    );
}

export default IntlWrapper;

3.6.2 Class组件

需要使用injectIntl的高阶组件包裹,如下为整体示例

如下为语言包文件zh_CN.js示例:

const zhCn = {
    'A>src.a.index>render.div.label': '展示内容示例',
};

export default zhCn;

如下为组件文件index.tsx示例:

import React, { Component } from 'react';
import { injectIntl } from '@cvte/cir-lcp-sdk';
import { WrappedComponentProps } from '@cvte/cir-lcp-sdk/src/interface/i18n';
import { IProps, IStates } from 'types';

class CustomComponent extends Component<IProps & WrappedComponentProps, IStates> {
    render(){
        const { intl } = this.props || {};

        return <div>{
            intl.formatMessage({
                id: 'A>src.a.index>render.div.label',    // 语言文件中翻译内容对应的key值
                defaultMessage: '展示内容示例'    // 此参数内会在当国际化文件加载异常或国际化provider异常时展示的缺省内容
            })
        }</div>
    }
}

export default injectIntl(CustomComponent);
// 如果此处导出时报类型错误可以尝试将组件类强转新类型,示例如下:
import { ComponentType } from 'react';
export default injectIntl(CustomComponent as ComponentType<IProps & WrappedComponentProps>)

3.6.3 Hook组件

需要使用useIntl的API,如下为整体示例

该示例中展示了如何传入变量类型替换国际化内容中的模版字符

如下为语言包文件zh_CN.js示例:

const zhCn = {
    'A>src.a.index>render.div.label': '第{page}页',
};

export default zhCn;

如下为组件文件index.tsx示例:

import React, { Component } from 'react';
import { useIntl } from '@cvte/cir-lcp-sdk';
import { IProps, IStates } from 'types';

const CustomComponent = (props: IProps) {
    const { pageNumber } = props || {};
    const intl = useIntl();

    return <div>{
        intl.formatMessage({
            id: 'A>src.a.index>render.div.label',    // 语言文件中翻译内容对应的key值
            defaultMessage: `第${pageNumber}页`    // 此参数内会在当国际化文件加载异常或国际化provider异常时展示的缺省内容
        }, {
            page: pageNumber,    // 该参数的key为国际化翻译内容中的待替换的模版字符串内容,值为替换后的变量
        })
    }</div>
}

export default CustomComponent;

3.6.4 普通方法

普通方法中使用国际化,由于没有react-dom介入,所以无法直接使用外层provider提供的注册机制。需要开发者手动的读取本地的国际化数据内容,其中分为如下两种方式。

使用@cvte/cir-lcp-sdk@1.3.2及以后版本提供的快捷工具

const zhCn = {
    'A>src.a.index>render.div.label': '展示内容示例',
};

export default zhCn;

如下为方法文件index.ts示例:

import { createIntlByCache } from '@cvte/cir-lcp-sdk/src/lib/locales/tools';

export const normalFn = () => {
  const intl = createIntlByCache();    // 创建一个国际化翻译单元

    return intl.formatMessage({
        id: 'A>src.a.index>render.div.label',    // 语言文件中翻译内容对应的key值
        defaultMessage: '展示内容示例'    // 此参数内会在当国际化文件加载异常或国际化provider异常时展示的缺省内容
    });
}

4. 支持国际化资源版本/npm版本

从天舟云1.9.1版本开始,天舟云预览系统支持国际化能力,配置系统暂未支持国际化能力

天舟云1.9.1中的预览系统资源列表 👉天舟云预览系统或接入业务系统依赖资源版本列表

如需要增加国际化能力,则需要升级天舟云预览系统或接入业务系统的如下的包和资源:

资源名称 版本 说明
cir-csb-generator 1.1.26.9 ⬆️
1.1.26.12
表单组件支持国际化翻译
viewTable 1.0.9.2 视图列表组件支持国际化翻译
cir-form-generator 1.0.14.2 表单详情组件支持国际化翻译
cir-page-generator 1.0.15.2 列表视图组件支持国际化翻译
cir-page-engine 1.0.0.5 工作台支持国际化翻译
cir-formtransfer 1.0.23.3 表单模型配置模块
cir-csb-common-view 1.0.15.2 表单渲染模版模块
Npm名称 版本 说明
@cvte/cir-lcp-sdk 1.3.25 天舟云开发者套件、国际化语言包
@cvte/cir-framework 4.0.12 CSB系统服务层
@cvte/cir-login 4.0.0 CSB系统登陆页面
@cvte/cir-antdframe 5.0.4 CSB系统页面框架层
@cvte/cir-tools 3.0.6 内核组件「附件上传、网络请求、页面资源加载」支持国际化翻译
@cvte/view-table 2.0.2 视图列表组件支持国际化翻译
@cvte/edit-ag-grid 3.0.1 可编辑表格工具库
@cvte/wuli-ag-grid 4.0.3 CSB底层表格工具库
@cvte/wuli-antd 3.0.6 CSB底层表单工具库
@cvte/cir-versions-log 2.0.1 CSB版本日志提示工具

5. 接入标准与使用规范

5.2 天舟云语言包编写

语言包文件类型为*.js,通过default返回国际化键值映射内容。

如下示例 zh_CN.js

const zhCn = {
    'cir-antdframe>src.index>screen.label.hello': '你好',
};

export default zhCn;

5.2.1 语言包中键名规范

如上示例中的键名:cir-antdframe>src.index>screen.label.hello可知。

键名分为三个部分:npm包/资源名称「驼峰转换」文件路径(「.」连接)翻译字段所在属性的指向链(驼峰转换),三个部分分别使用「>」符号进行链接。

5.2.2 为什么我们会选择这样的键名规范?

当前的天舟云由CSB系统衍生而来,其中有较多历史性的模块,也有非常多新开发的模块,甚至还会有其他小伙伴开发的自定义组件和自定义内容。

如果我们只是按照内容的异同性去编写国际化文件,那样会导致后续维护内容时需要格外的注意和小心。

同时在不同的地区场景下,可能会由于合规、人文、业务化的要求,需要的翻译颗粒度非常细,不同的词语和段落面向不同地区的使用者时,会非常谨慎。

由此我们从控制翻译内容颗粒度的角度出发,选择用模块、文件路径、内容类xpath的手段来定位翻译的内容,给后续未来的翻译内容拓展性预留较大的变化空间

当然,这样的方案也不是最优的方案,我们也期待其他的小伙伴能提供更多的思路,一起解决未来可能出现的问题。

5.3 设置默认语言

通过Apollo配置中的i18n.language.key设置系统默认的国际化语言键名

5.4 动态改变语言与监听

动态改变语言的机制基于发布订阅模式,通过事件注册机制实现动态修改。

事件名:onI18nLanguageChange

事件触发参数列表:

参数 类型 说明
locale string 国际化语言包键名
antLocale Locale ant-design 组件库国际化语言集
customLocale Record<string, any> 天舟云自定义国际化语言集
antLocaleGenerator (antLocalesMap?: Record<string, Locale>, locale?: string, extra?: Record<string, any>) => Locale ant-design 组件库国际化语言集生成方法
customLocaleGenerator (customLocalesMap?: Record<string, any>, locale?: string, extra?: Record<string, any>) => Record<string, any> 天舟云自定义国际化语言集生成方法

改变语言代码示例:

import { Message, i18nEventKey } from '@cvte/cir-lcp-sdk'
import { II18nEventData } from '@cvte/cir-lcp-sdk/src/interface/i18n'

Message.emit<II18nEventData>(i18nEventKey, {
    data: {
        // 当前选择地区语言,如果只传该参数,则使用默认的该地区语言对应的语言包内容
        locale: 'en-US',
        // 自定义ant-design所对应的当前地区语言的语言包内容
        antLocaleGenerator: (antLocales) => {
            // antLocales为默认的antd语言包
            return antLocales['en-US'];
        },
        // 自定义当前整体系统所对应的当前地区语言的语言包内容
        customLocaleGenerator: (customLocales, locale, extra) => {
            // 获取当前系统的国际化配置
            const { localeConfig } = extra || {};
            // 获取当前系统的国际化配置中的自定义国际化语言集
            const { customLocale } = localeConfig || {};
            return {
                ...(customLocale || {} ),
                'cir-antdframe>src.index>screen.label.hello': 'Hello',
            }
        }
    } as II18nEventData
})

监听语言改变代码示例:

import { Message, i18nEventKey } from '@cvte/cir-lcp-sdk'
import { IMessageBody } from '@cvte/cir-lcp-sdk/src/interface/message'
import { II18nEventData } from '@cvte/cir-lcp-sdk/src/interface/i18n'

// 监听器实现方法
const listener = async (msg: IMessageBody<II18nEventData>) => {
    const { locale: _locale, antLocale: _antLocale, customLocale: _customLocale, antLocaleGenerator, customLocaleGenerator } = msg.data || {};
    // TODO: 监听实现内容
}
// 启动监听器
Message.on<II18nEventData>(i18nEventKey, listener);

// 移除监听
Message.remove(i18nEventKey, listener);

5.5 自定义国际化

如果需要自定义国际化的初始化过程,可建立文件app/modules/App/locales/index.ts

该文件需要实现一个基于IInitLocale接口的类,同时须导出类的一个实例

初始国际化的执行时机是在CSB工程的app/modules/App/global.js执行之后

其中会有一些关于apollo存储字段在页面中的标签id,开发者可根据实际情况修改标签id内容

❗️❗️❗️需要注意的是:如果开发者的自定义流程有任何异常,框架会放弃自定义的内容,而使用默认的国际化内容和初始化流程

文件示例如下:

/* eslint-disable global-require */
/* eslint-disable import/no-dynamic-require */
import CirCache from '@cvte/cir-cache';
import { i18nLanguageMemoName } from '@cvte/cir-lcp-sdk';
import { IInitLocale } from '@cvte/cir-lcp-sdk/src/interface/i18n';

class InitLocale implements IInitLocale {
  /**
   * 本地缓存实例
   *
   * @memberof InitLocale
   */
  cirCache = new CirCache({ storage: 'memory' });

  /**
   * 页面中缓存国际化语言键名的标签id
   *
   * @memberof InitLocale
   */
  i18nLanguageElmName = 'i18nLanguageKey';

  /**
   * 页面中缓存国际化语言键名列表的标签id
   *
   * @memberof InitLocale
   */
  i18nLanguageKeysElmName = 'i18nLanguageKeys';

  /**
   * 页面中缓存国际化语言包名列表的标签id
   *
   * @memberof InitLocale
   */
  i18nLanguageFileNamesElmName = 'i18nLanguageFileNames';

  /**
   * 国际化语言包文件名展示文本集合的标签id
   *
   * @memberof InitLocale
   */
  i18nLanguageLabelsElmName = 'i18nLanguageLabels';

  init() {
    const i18nLanguage = this.cirCache.getValue<any>(i18nLanguageMemoName);
    // 2023.05.23 增加国际化语言包键名标识、国际化语言默认设置的input元素id、国际化改变监听事件的事件名称 --yuanzihan
    const i18nLanguageKeyElm = document.getElementById(this.i18nLanguageElmName);
    const i18nLanguageFileNamesElm = document.getElementById(this.i18nLanguageFileNamesElmName);
    const i18nLanguageKeysElm = document.getElementById(this.i18nLanguageKeysElmName);
    const i18nLanguageLabelsElm = document.getElementById(this.i18nLanguageLabelsElmName);
    if (i18nLanguageKeyElm) {
      i18nLanguage.key = i18nLanguageKeyElm.getAttribute('value');
      i18nLanguageKeyElm.remove();
    }
    if (i18nLanguageFileNamesElm) {
      i18nLanguage.fileNames = i18nLanguageFileNamesElm.getAttribute('value');
      i18nLanguageFileNamesElm.remove();
    }
    if (i18nLanguageLabelsElm) {
      i18nLanguage.labels = i18nLanguageLabelsElm.getAttribute('value');
      i18nLanguageLabelsElm.remove();
    }
    if (i18nLanguageKeysElm) {
      i18nLanguage.keys = i18nLanguageKeysElm.getAttribute('value');
      i18nLanguageKeysElm.remove();
    }
    this.cirCache.setValue(i18nLanguageMemoName, i18nLanguage);
  }

  generateIntlMessage() {
    const intlMessage = {};
    const i18nLanguage = this.cirCache.getValue<any>(i18nLanguageMemoName);
    const { keys, fileNames } = i18nLanguage || {};
    const keysArr = `${keys || ''}`.split(',');
    const fileNamesArr = `${fileNames || ''}`.split(',');
    keysArr.forEach((key, i) => {
      const fName = fileNamesArr[i];
      const contentFn = () => require(`./${fName}`)?.default;
      const result = contentFn();
      intlMessage[key] = result;
    });
    return intlMessage;
  }
}

export default new InitLocale();

5.6 获取当前国际化全局数据

除了通过事件监听触发来获取国际化全局数据以外,也可以直接从缓存实例中主动获取当前的国际化全局数据。

具体示例如下:

import { Cache, i18nLanguageMemoName, II18nLocalData } from '@cvte/cir-lcp-sdk';

const cache = new Cache({ storage: 'memory' });
const i18nLanguage = cache.getValue<II18nLocalData>(i18nLanguageMemoName);

6. 常见问题与解决

如何知道当前页面加载的所有国际化内容?

问题

从哪里可以知道当前页面国际化的字段是否有缺失?

解决

在页面的控制台中输入window['cir-cache'].get("cir-cache-i18nLanguage").value,即可查看当前页面的国际化会话缓存内容。

语言包大小影响系统启动速度吗?

问题

语言包体积增加后,影响系统首开屏展示时间吗?

解决

会对运行时产物的体积有影响

以下为@宇阳同学基于ltc-cbp工程做的一个体积测试和结果展示

当有总数20万条各类语言翻译数据时,首开屏会滞后约1秒,项目打包后体积增加20mb

无法正常加载<IntlProvider></IntlProvider>组件

问题

框架系统的IntlProvider组件未能被正常初始化

解决

可能性一:未能正常开启国际化

cir-framework中会根据当前是否开启国际化来选择不同的渲染内容:

解决办法:请根据上文内容,检查各环节的国际化配置是否正常。

可能性二:宿主系统存在多个不一致的react-intl版本

cir-frameworkcir-lcp-sdk中会内置react-intl3.12.1版本,宿主系统可能有安装其他!=3.12.1的版本

解决办法:保持宿主系统下的react-intl版本一致,避免不同版本带来的多实例情况

可能性三:使用的组件外层没有包裹IntlProvider

使用的组件/方法中直接使用了intl实例,而没有在外包裹IntlProvider,或由于某些原因intl实例的使用域逃逸了根IntlProvider

解决办法:在cir-lcp-sdk1.3.14版本及其之后,内置了IntlWrapper,用于接入方快速接入<IntlProvider></IntlProvider>

只增加某一个语言包后不翻译

问题

CSB系统下只增加了一个语言包文件,但系统未进行正常翻译

解决

CSB系统下,底层框架读取国际化文件是各个语言区域一并读取,如果缺少某一个语言包,则会导致整个国际化接入失效。

解决方案为增加全部的区域语言包文件。

React Hook中使用了useIntl但未生效

问题

使用useIntl返回的intl实例时,被包裹在了一个useMemouseEffectuseCallback等HookAPI函数中,但未观察intl实例是否发生变化。

示例代码:

import React from 'react';
import { useIntl } from '@cvte/cir-lcp-sdk'

const DemoComponent = (props) => {
    const intl = useIntl();

    const CptMemo = useMemo(() => {
        return <div>{
            intl.formatMessage({
                id: 'A>src.a.index>render.div.label',    // 语言文件中翻译内容对应的key值
                defaultMessage: '展示内容示例'    // 此参数内会在当国际化文件加载异常或国际化provider异常时展示的缺省内容
            })
        }</div>
    }, [])

    return CptMemo;
};

export default DemoComponent;

解决

在使用intl的地方,如果被包裹在HookApi函数中,需要观察intl实例是否发生变化。否则在动态切换全局语言时,由于HookApi的缓存性,会将新的变化忽略。

示例代码:

import React from 'react';
import { useIntl } from '@cvte/cir-lcp-sdk'

const DemoComponent = (props) => {
    const intl = useIntl();

    const CptMemo = useMemo(() => {
        return <div>{
            intl.formatMessage({
                id: 'A>src.a.index>render.div.label',    // 语言文件中翻译内容对应的key值
                defaultMessage: '展示内容示例'    // 此参数内会在当国际化文件加载异常或国际化provider异常时展示的缺省内容
            })
        }</div>
    }, [intl])

    return CptMemo;
};

export default DemoComponent;
作者:袁子涵  创建时间:2024-09-19 11:41
最后编辑:袁子涵  更新时间:2025-05-12 18:04