什么是国际化
对于国际化的理解,狭义上可以指产品提供除了本地外的其他国家的文案的能力,但更准确的定义应该是“国际化是让产品和服务能够适应不同国家地区,打入它们的市场”。
之所以不能讲国际化开发等同于【逐字替换】,也是因为除了文字,对于不同国家地区,对日期、时间的展示,数字计数方法(如英文环境下每隔3位分隔:1,000),量词的显示(英文的单、复数和特殊复数)等等问题,都是国际化开发和设计中的考验。
我们的产品在开发初期,就着眼于国际市场,所以在产品设计之初便考虑国际化,但开发过程中对多语言的配置,属于是【高频重复、非自动化、分散易混乱】的,对于这种特点的工作内容,我们往往需要考虑是否有更高效的方式,能够【提升开发效率、降低出错率、优化开发体验】。
常见的国际化开发
Antd的国际化配置
antd 提供了一个 React 组件 ConfigProvider 用于全局配置国际化文案。
import zhCN from 'antd/lib/locale/zh_CN';
return (
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
);
在全局配置中引入语言包,即可使antd按对应语言展示。
React-intl
React-intl是雅虎的语言国际化开源项目Format.JS的一部分,通过其提供的组件和API可以与ReactJS绑定。
引入react-intl,由于IntlProvider包裹一次即可生效,把它包裹在系统根组件最外层即可。
api.locale用于国际化数字、日期等,默认为en,这里设置成浏览器语言;
api.messages接受的是一个对象,即引入的语言包。
国际化开发场景
格式化字符串
由上述内容可知,本地语言配置集中在‘src/locales/..’目录中,在最外层被引入执行一次。
对仅支持中英文的开发场景为例,开发人员会同时打开en/xModule.ts和zh/xModule.ts文件,配置相同格式&不同语言内容的文件,手动保持一一对应。
// en-US.ts
export default {
'xModule.title': 'My Title',
};
// zh-CN.ts
export default {
'xModule.title': '我的标题',
};
完成静态配置后,在视图内使用:
import { FormattedMessage } from 'react-intl';
export default () => {
return (
<div>
<FormattedMessage id="xModule.title" />
</div>
);
};
可以总结出如下问题:
- 三个文件的国际化文字只是内容一样的字符串,可以说只是一种约束,没有通过模块化配置进行关联;
- 为保证唯一,配置时通常会包含前缀,但使用时在该模块page下,其实可以忽略前缀的含义;
- 在开发过程中id形如魔法属性,不知道实际代表什么内容,只能通过命名猜测,增加后续开发者维护难度;
- 使用时必须打开配置文件查找配置,无自动提示,开发效率低。
一个初步的开发流程优化
/**
@props.config 自定义JSON
@props.prefix 和模块文件名相关的前缀
@return { zhConfig, enConfig }
*/
import languageJsonDealer from 'src/utils/languageJsonDealer';
export const prefix = 'xModule';
export enum XModuleLangEnum {
/** 标题 */
title = 'title',
}
const XModuleJSON = {
[XModuleLangEnum.title]: { zh: '我的标题', en: 'My Title' }
}
const { zhConfig, enConfig } = languageJsonDealer({ config: XModuleJSON, prefix });
export default zhConfig;
export { enConfig };
export { enConfig as default } from '../../zh/xModule';
import { FormattedMessage } from 'react-intl';
import { prefix, XModuleLangEnum } from 'src/locales/zh/xModule';
export default () => {
return (
<div>
<FormattedMessage id={`${prefix}.${XModuleLangEnum.title}`} />
</div>
);
};
总结如下优化点:
- 提取公共前缀prefix为基本配置,在每个文件中只需做焦点属性定义,languageJsonDealer接收并拼接前缀,保证全局下各配置不会冲突;
- 书写枚举XModuleLangEnum,并通过模块化导出和引入,在定义和使用时均通过该枚举,规避静态约束带来的问题,显著表现于翻译后端枚举内容的效果;
- 并对枚举每行进行魔法注释/** desc */,可以达到编译器自动提醒,使用时通过XModuleLangEnum.x即可查看注释含义,并且无需再次打开配置寻找id名;
- 自定义JSON格式,属性对应的中英文写在一处,更好的关联多语言对应内容,便于UI检查,并且提升了开发速度,如若以后支持3、4...N语言,只需增加languageJsonDealer能力即可;
附:对于No.2枚举的使用示例
import languageJsonDealer from 'src/utils/languageJsonDealer';
export const prefix = 'xModule';
export enum XModuleLangEnum {
/** 性别 */
male = 'male',
}
const XModuleJSON = {
[XModuleLangEnum.male]: { zh: '男', en: 'Male' }
}
const { zhConfig, enConfig } = languageJsonDealer({ config: XModuleJSON, prefix });
export default zhConfig;
export { enConfig };
import { XModuleLangEnum } from 'src/locales/zh/xModule';
export const SexEnumMap = {
/** 性别 */
1: XModuleLangEnum.male,
}
import { FormattedMessage } from 'react-intl';
import { prefix, XModuleLangEnum } from 'src/locales/zh/xModule';
import { SexEnumMap } from 'src/interface/xModule.ts';
export default (props) => {
// eg: value = 1;
const value = props.apiRespons?.value;
return (
<div>
<FormattedMessage id={`${prefix}.${SexEnumMap[value]}`} />
</div>
);
};
开发效率对比
1.将重点内容集中在单个文件中(如:中文开发人员可选zh/xModule.ts,英文开发人员可选en/xModule.ts),配置时定义好key,从原型文档和UI设计稿中复制粘贴更迅速;
2.完成配置后,任意处使用时,均可通过编译器自动化完成,提升开发体验,达到0出错率,便于团队协作,提高交叉开发效率;
3.只在根目录引入时一次处理,通过模块化引入使用,并不会浪费过多内存;
待提升方向
- 核心内容为key枚举和自定义JSON,是否有可能将基础物料,通过市面上某种可视化工具(低代码、0代码、编译),交由产品/UI来自动生成,达到开发人员0配置;
- prefix使得开发过程中固定了所import的模块,此方式否是团队能接受的模式;
- 根据模块可选的模式,是否可以抽取公共内容,如何抽取或约定,不同模块相同焦点key是否还能做更多的场景处理;
- 枚举和注释的书写可能增加了一些工作量,是否还有其他更好的方式;
- kivi
- webpack loader
日期时间
用于格式化日期,能够将一个时间戳格式化成不同语言中的日期格式。
用于格式化时间,效果与相似。
通过这个组件可以显示传入组件的某个时间戳和当前时间的关系,比如 “ 10 minutes ago"。
数字量词
这个组件最主要的用途是用来给一串数字标逗号,比如10000这个数字,在中文的语言环境中应该是1,0000,是每隔4位加一个逗号,而在英语的环境中是10,000,每隔3位加一个逗号。
这个组件可用于格式化量词,如英文的语言环境中,描述单数与复数是有区别的,这个组件的作用就在于此。传入组件的参数中,value为数量,其他的为不同数量时对应的量词,在下面的例子中,一个的时候量词为message,两个的时候量词为messages。实际上可以传入组件的量词包括 zero, one, two, few, many, other 已经涵盖了所有的情况。
<FormattedPlural value={10} one='message' other='messages'/>
利用VSCode插件 - i18n Ally
其实我们的最终目标,就是希望编译器能够提示,国际化字符串ID代表了什么具体内容,所以编译器插件就是最好的解决方案。启用i18n Ally可以通过在项目.vscode文件进行设置,这样可以更好的使团队开发一致,根据官网描述添加配置项,重点需要关注的是解析路径,通过路径匹配器对项目进行目录结构描述、文件命名和命名空间映射,以此告知扩展如何处理自定义案例。
{locale}/{namespace}.{ext} # matches "zh-CN/attributes.yaml"
{namespaces}/{locale}.{ext} # matches "common/users/en-US.json"
{locale}.json # matches "fr-FR.json"
// ├─.vscode
// ├─public
// └─src
// ├─assets
// └─i18n
// ├─config.ts //配置
// ├─index.ts //国际化初始化
// └─locales //语言
// ├─en-US
// └─zh-CN
"i18n-ally.enabledParsers": ["json", "js", "ts"],
"i18n-ally.pathMatcher": "{locale}/{namespaces}.js",
// Matched result
{ locale: 'en-US', namespace: 'x.js' }
综上所述,建议使用【i18next + react-i18next + i18next-browser-languagedetector】处理国际化。
语言环境文件中的键样式:nested( {"a": {"b": {"c": "..."}}})。
格式化内容时,重点关注 import { useTranslation } from 'react-i18next' 的内容,该hook返回{t: 格式化方法, i18n: 其他国际化相关api对象 }。
vscode搜索并安装插件 -- i18n Ally
可使国际化key按设置的zh-CN中文展示,提升开发体验及效率,目前已配置实时刷新读取国际化键值用于展示。配置国际化可在文件中直接书写(如:t('app.newKey')),点击✏️铅笔按钮可直接书写对应中英文,并指定写入文件的路径。使用国际化内容时,编译器也会自动展示当前语言环境的内容。
项目实际开发的场景及期望
- 翻译交由三方直接处理,在写完zh文件后,直接交给翻译人员,处理出其他单独语言文件
- 期望引入编译提醒、注释、类型
- 简化枚举写法
可行方案:
import { getSingleLangWithPrefix } from 'src/utils/languageJsonDealer';
export const prefix = 'xModule';
export enum XEnum {
/** 标题 */
title,
}
const XModuleJSON = {
[XEnum.title]: '我的标题'
}
const config = getSingleLangWithPrefix({ config: XModuleJSON, prefix });
export default config;
import { XEnum, prefix } from '../../zh/accountBalance';
import { getSingleLangWithPrefix } from 'src/utils/languageJsonDealer';
const XModuleJSON = {
[XEnum.title]: 'My Title'
}
const config = getSingleLangWithPrefix({ config: XModuleJSON, prefix });
export default config;