- 1. 天舟云前端国际化架构设计
- 1.1 现支持的国际化语言包键名映射
- 2. 天舟云集成
- 2.1 Apollo配置
- 2.2
config/server.js
修改 - 2.3 开启顶部工具栏切换语言
- 3. 业务系统集成
- 3.1 npm包升级
- 3.1.1 CSB系统
- 3.1.2 非CSB系统
- 3.2 Apollo配置
- 3.3
config/server.js
修改 - 3.4 资源中心依赖共享
- 3.5 国际化文件
- 3.5.1 CSB系统
- 3.5.2 非CSB系统
- 3.5.3 国际化语言包内容
- 3.6 组件接入国际化
- 3.6.1 准备工作
- 3.6.2 Class组件
- 3.6.3 Hook组件
- 3.6.4 普通方法
- 4. 支持国际化资源版本/npm版本
- 5. 接入标准与使用规范
- 5.2 天舟云语言包编写
- 5.2.1 语言包中键名规范
- 5.2.2 为什么我们会选择这样的键名规范?
- 5.3 设置默认语言
- 5.4 动态改变语言与监听
- 5.5 自定义国际化
- 5.6 获取当前国际化全局数据
- 6. 常见问题与解决
- 如何知道当前页面加载的所有国际化内容?
- 问题
- 解决
- 语言包大小影响系统启动速度吗?
- 问题
- 解决
- 无法正常加载
<IntlProvider></IntlProvider>
组件 - 问题
- 解决
- 只增加某一个语言包后不翻译
- 问题
- 解决
- React Hook中使用了
useIntl
但未生效 - 问题
- 解决
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-framework
、cir-lcp-sdk
中会内置react-intl
的3.12.1
版本,宿主系统可能有安装其他!=3.12.1
的版本
解决办法:保持宿主系统下的react-intl
版本一致,避免不同版本带来的多实例情况
可能性三:使用的组件外层没有包裹IntlProvider
使用的组件/方法中直接使用了intl
实例,而没有在外包裹IntlProvider
,或由于某些原因intl
实例的使用域逃逸了根IntlProvider
解决办法:在cir-lcp-sdk
的1.3.14
版本及其之后,内置了IntlWrapper
,用于接入方快速接入<IntlProvider></IntlProvider>
只增加某一个语言包后不翻译
问题
CSB系统下只增加了一个语言包文件,但系统未进行正常翻译
解决
CSB系统下,底层框架读取国际化文件是各个语言区域一并读取,如果缺少某一个语言包,则会导致整个国际化接入失效。
解决方案为增加全部的区域语言包文件。
React Hook中使用了useIntl
但未生效
问题
使用useIntl
返回的intl
实例时,被包裹在了一个useMemo
、useEffect
、useCallback
等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;
最后编辑:袁子涵 更新时间:2025-05-12 18:04