🌍 Web 国际化一文通

328 阅读18分钟

引言 - Intro

本文主要根据我在公司内分享的国际化工程化文档,尽量去除内部部分,以开源方案为主,具体介绍 web 项目的国际化整体方案。

不会贴多少代码,从工程化的角度,旨在介绍前端项目国际化的基本思路。为了尽可能详细、宽泛地介绍其中基础要点,不可避免的会导致内容偏多。

附上文中会用到的一个小的语言转换 demo github.com/xiaxzp/xzp_…

为什么会有这篇文档?国际化方案文档还不够多吗?

Web 的国际化问题由来已久,方案诸多,每个问题总归都可以找到解决方法。如果我们直接搜索国际化方案,我相信绝大多数情况我们会得到一个针对语料、文案处理的具体实现方法,甚至是某个工具、平台的推广文。

然而作为一切的前提,即工程国际化的基础知识、基本方案、甚至在“什么应该是问题”这点上,可以说一直是一个视野盲区,我们很难找到一个完整的工程化方案!

当一个从未接触过,或是做过一点相关内容的同学开始做国际化的时候,认知上难免会有遗漏与偏差。内部包括我们在内的很多平台,在公司国际化刚起步时其实也没什么经验,我们也在搜索已有的国际化方案,大多数情况下我们只知道文字需要翻译,没有人是这方面的专家!经过很长一段时间后才逐渐意识到,原来一些语种的数字格式甚至都不一样!

国际化业务往往需要针对多地区用户,配置不同的语言环境与展示方式,适配各种国际标准。其中会包含:多平台文案多语言、数字多语言、日期本地化、RTL 阅读顺序、Region code 管理、字体管理、货币管理、手机区号管理等等。

当然,最最重要的还是要培养国际化意识!

恰逢我们新项目起步,本文收集了新项目从 0 到 1 国际化过程中,具体需要处理的大部份事物,以及遇到的各种各样的问题,一方面作为我们组的技术沉淀,另一方面也希望能帮助每一个国际化项目查漏补缺、改进流程、渡过难关。

起点 - Codes

想做国际化,map、映射不会少

推荐使用开源包,自己维护比较麻烦,毕竟。。。谁也不能确定未来哪天这东西会不会变,直接更新包总归比没头没尾地维护 JSON 好

Country / Region code

建议不要单独使用 country 这个单词,容易产生争议

这个代码代表了各个国家/地区,及 geo 中的 level0,符合国际标准 ISO_3166-2

它对于选择用户所在地区、推导用户使用的手机区号、推导用户喜好地区、货币币种等等都有协助作用。

可以自己维护 map,也建议直接使用 country-list 包或其他类似的,它参照国际 ISO 标准提供了 code 和英语名称,以及一些简单的函数,size 10kb+,可以直接用

Phone code

大部分情况下,phone code 与 country / region code 是 1:n 关系。

譬如 +1 这个区号不只对应 US,也可以对应其他多个地区。一般来讲 country / region code 仅用于展示与确定默认值。

同时,有些地区手机号还有特殊的校验规则。

libphonenumber-js 这个库是谷歌 libphonenumber 的 js 版,里面提供了丰富的 api 来查找、处理、校验号码,唯一的问题是 size 会大一些 (100+kb),但实际上我们的一些其他依赖包已经在使用了,所以在资源重复利用的情况下也不是问题。

Currency

货币代码符合 ISO_4217 标准,在 JS native 中的 INTL 中其实有 currency code。

很多时候我们希望自主维护一套货币与货币符号的展示方式。

货币与地区基本上是多对多关系,和手机号类似,除了取默认值与业务限制外,探寻货币与地区的绑定关系几乎没什么意义。

推荐直接使用 npm 包 currency-symbol-map & country-to-currency 对标国际 ISO 标准,两个包各 < 10kb 且提供一点简单的查询函数。前一个包可以根据 currency code 查询 currency symbol,而后一个可以查到 region code 对应的最常用的 currency code。

Language code

这个是国际化过程中最重要的点,各方的标准可能都不一致。

国际标准 ISO_639 最初只定义了小写字母代码,实际上,一个 639 代码可以进一步扩展为书写系统的 xx-XX 格式。

举个例子,中文。中文都可以叫 zh ,它无法区分简体中文与繁体中文。于是我们进一步把它区分为了 zh-CN 与 zh-TW,或者 zh-Hant 与 zh-Hant-TW 甚至 zh-Hans。

过度探究书写系统毫无意义。我们可以在原生 js navigator 找到 639 小写代码,获取用户当前语言。至于具体对应到代码里什么 key,这个 map 仍需自己维护。同时,我推荐在 html 上设置属性 lang = navigator lang code 以享用一些可能的原生能力。

至于用户设置的这个字段保存在哪里,从哪里读取?建议至少在 cookie 有一份,这样 server 或许也能用上。

标准 - Numbers & Date

数字 / 货币

通常国际化项目组都会针对性地封装对应框架的货币数字输入、展示的组件

对于一些语种,数字并不是 12,345.67 的格式。譬如法语中,逗号与点号的意思是相反的 (12.345,67),也有一些地区以空格代替逗号,甚至有的地区不使用阿拉伯数字。

在数据存储中一般使用标准阿拉伯数字格式,也就是 12345.67 格式,而展示给用户时,就需要展示层对它进行处理。

切记数字国际化仅用于展示,不能进数据流,计算机是美国人发明的,我们的数据系统不能识别某些国际化后的数字!


浏览器 native 自身有一些函数来处理展示问题。

toLocaleString

浏览器对 Intl.NumberFormat 的简单包装,支持按照语言格式化数字,并提供了 NumberFormat options ,包括精度、货币等的选项。Intl.NumberFormat 和包装的主要区别,在于处理大量数字时,可以通过先创建这个结构体来提高运行效率。

这些函数用来处理展示问题。在货币与输出格式上,设计往往会整些不一样的规范,js native 函数不能做更进一步的定制。

还有一个问题在于,js native 对于用户输入的处理,依然基于浏览器语言。譬如 Number('123.4') 在我们的语言环境下可以识别,但 Number('123,4') === 'NaN'。js native 没有一个好的方法来处理这个问题。

于是我们转向了开源库

Globalizejs

一个老牌开源库,里面日期、数字、货币啥都有,它更像一种 native 方法的扩充,如果对展示的要求不高是可以试试的。不过它的用法仅对于我们团队来说是比较晦涩的。

Numeral.js

老牌数字格式化库,文档简洁,用着很爽😋

一个问题是它的多语言语料不足。对于大部分的使用阿拉伯数字的地区,这个问题可以通过截取 native INTL 来处理。如果追求完美,还是需要精细化处理的。

const res = intl
  .format(12345.678) // es: 1234,567 12.345,678
  .match(/^\d{2}(.)?\d{3}(.)?\d{3}$/);
const [, thousands = ',', decimal = '.'] = res || [];
numeral.register('locale', code, {
  delimiters: {
    thousands,
    decimal,
  },
  ...
)

Example

EN

FR

日期

和数字的问题类似,对于不同的语种,周一到周日的称呼、短写、日期时间排列顺序都不一样。

toLocaleString

同样,native 自己有解决方案。它包装 Intl.DateTimeFormat - JavaScript | MDN ,区别和上文数字一样,问题也几乎一样。它在遇到业务定制时比较无力。

Globalizejs

和上一节一样不赘述了

Dayjs

有时候开源库不能完全支持业务需要,部分项目组会开发自己的插件,或者不同的封装

dayjs 支持的语言包还算比较全面,体积小,支持插件拔插。

它的格式化几乎能覆盖全部的设计要求,如果不符合建议直接解决设计,当然大概率是解决不了设计的,我们可以自己改改插件🌛。

我实际遇到的是它 localizedFormat 插件 (也就是我们最喜欢用的 format('LLL') 这种) 在部分语种用了 12 时制,可以把它复制出来自己改一下。

Example

EN

FR

文案 - Keys & Trans

这是国际化项目最主要的部分,内容会比较多

多语言的过程,就是 key + language code + 语料 = 最终渲染的文案

翻译框架

前端主流项目框架基本来说就是 Vue 和 React 了。

 Vue 对应 vue i18n,一如既往地继承了 vue 上手快速容易的特性。

React 使用 i18next + react-i18next 插件组合,在 hooks 编程模式下也算方便。

我们内部主流使用 React,下文也主要侧重 React 环境叙述。

框架实例化

一般来讲,我们把一个翻译模块称为一个 I18n 实例。

对于纯粹单个 SPA 项目来说,一个 I18n 实例足以满足需求,并且各种 i18n 包们也普遍提供了一个默认的实例。

但我必须强调,我们要自己创建新的实例,不直接使用包里的。

这里讲讲我以前遇到的一个 bug。

某 React 框架下的平台直接使用 i18n 包的默认实例,平时跑着没什么问题。某次需求中引入了一个新的 sdk,偶尔出现整个平台的翻译全没了的问题。排查下来发现,该 sdk 依赖同样直接使用 i18n 包的默认实例,导致共用了平台 i18n 包的默认实例,两份代码竞争覆盖了实例的文案。

对于 react hooks,我们同样要注意,在使用默认选项的情况下,最好不要存在多个实例。

以最常用的 useTranslation hook 为例子,我们看一下它的源码:

react-i18next 搜索 i18n 实例的方法为 props > context > 缓存,如果不提供 props / context 又出现了多次创建实例的情况,那么会导致它使用的 i18n 实例不符合预期。当然能强绑定 I18nContext 是最好的

响应式渲染

一个 I18n 平台最基本的能力是,切换语言。

切换语言简单来讲就是设置平台的语言 key,然后更新 dom,i18n 框架都会提供初始化、设置、切换语言的 API。


更新的两种基本方式,一种是缓存语料后再加载 / 刷新页面,一种是利用框架的依赖收集热更。

首先我们相信一个正常用户,不会频繁地去切换语言。谁会没事干切来切去玩?只可能是内部走查人员。

先谈谈框架中的语料更新。

对于简单页面、静态页面来说,缓存文案再渲染页面最方便,用最简单的方式使所有东西按照正确的文案渲染。切换语言时刷新页面即可。这也是大部分纯 ssr 页面的渲染方式。

对于有一定业务逻辑的、有 csr 能力的页面来说,这个操作的体验较差。我们很可能会想到一些针对语料的体验优化和首屏优化,不让页面加载时等待语料 loading,想要语料在异步热更新。

譬如后文会说到文案远端更新的问题,我们总不可能在语料下载完之后自动刷新页面吧?让用户等着 loading 也不好吧?且不排除会遇到一些需要动态修改语料库的情况。所以能做响应式更新的复杂项目我推荐一定要实现依赖更新的 re-render,而不是通过等待或是刷新。

对于语言切换的场景,响应式地切换或刷新其实差别不大,根据业务需求选择即可。我们可能会遇到一些不受前端框架控制的文案,譬如与后端交互直接传递的文字等。热更在保留用户操作上体验更好,而刷新往往可以保证项目整体使用正确的语言。


那么再说回响应式渲染的****具体使用方式

首先框架的 i18n 实例都会以某种方式构建对应框架的依赖收集与更新。

vue2 的 vuei18n 会自己创建一个 vue 实例响应,而 react-i18next 会使用 react hooks 或者 provider / context。它们保证在自己的语料、语种发生更新时,触发依赖更新。

旧框架,如 vue2 在原型链、react class component 通过 provider,提供 t 函数或者 / 组件来提供匹配框架依赖收集的多语言渲染方式。当 i18n 实例更新时,就会触发这些组件的重新渲染。

新框架依赖 composition api / react hooks 实现类似的逻辑,这里就不深究框架的响应式原理了,这种方式进行翻译渲染更加灵活。


说说更通用的情况

我们有时需要在一些普通 js / ts 文件中获取文案,这时可以直接调用 i18n 实例的 t 渲染函数

但 t 它只是一个函数,如 vue2 中它是 vue 实例的一个 function,在 react 中它只是 i18next 实例的 function。在不同的框架下,因为依赖更新的方式不同,它可能并不能靠自身进行重新渲染。

如 vue2 中的 i18n 实例,因为它的实现方式是通过创建一个 vue class 实例,在 vue 的框架内使用时,因为 vue 的依赖收集方式,t function 是可能被收集到依赖并实现自更新的。

又如在 react 直接使用的 i18next 实例,直接调用的 t 只是一个 function,react 不会对它进行依赖收集,不通过 hooks 或者 provider 根本不可能实现变量自更新。但我们依然可以通过 react i18n hooks 更新导致的组件重新渲染,像副作用一样来重新调用并更新这种方式获取的文案。

顺便聊聊一个 bug

某次我们发现 react 有一段文案,在文案更新后,没有重新渲染。排查后发现,这段文案在某个 ts 文件中,以类似 const xxx = i18n.t('xxx') 的方式缓存了。知晓文案更新逻辑后,我们自然了解到,副作用不能让一个缓存值更新。

解决方法也很简单,要么改成 单纯缓存 key + 组件中渲染,要么改成 const xxx = () => i18n.t('xxx') 的 runtime 运行时形式。

EN

FR

语料渲染

想做翻译,先得有语料。

之前提到了语料更新上的一些问题。对于一些不计较大小的客户端软件、或是纯服务渲染的页面,语料只需要事先放在本地文件里就好。不过如果没有热更机制,每次更新都得发版。一般对于服务端来说,定期拉取刷新本地文件缓存即可。

对于有性能要求的、即开即用无需下载的页面类,显然把所有语料塞在包里是不太合适的,但啥也不放显然也是不合适的。同时,我们在多职能团队合作过程中,也需要一个工具、平台来维护语料,毕竟 text key 多起来谁也不知道是啥了。

本文接下来统一使用“🐦平台”抽象代指某文案管理平台,本章也会基于 🐦平台 做一些简单的语料管理的介绍。

语料缓存与更新

前文提到,诸如 web 页面这种无需下载、有性能要求的产品,不好把全部语料塞代码里。可如果我们什么也没有,也不强制等待下载,那么首屏出来的很有可能全是 key,很容易让人觉得是 bug。

那么我们要解决几个问题。

  1. 如何又快又好地保证首屏?

    1.   最好的方式当然是,语料塞代码里,当然不是指把所有语料都塞进去。英语是世界上使用最广泛的语言,我们完全可以只把英语塞进代码里,甚至只有首屏需要的英语语料,并把英语作为兜底语言。这样可以保证首屏不会出现展示 text key 的情况。
  2. 如何安全地更新语料?

    1.   这时候就要讲到语料管理平台。🐦平台自有一个语料录入端,并提供 sdk 获取语料。业务方通过 sdk 的 http 接口请求,能够从远端获取更新语料,使语料更新与代码解耦。考虑到包体积与网络状况,🐦平台可以通过 localstorage 缓存语料(不过 localstorage 存在存储上限,也可以用 indexdb 作为替代 GitHub - localForage/localForage: 💾 Offline storage, improved. Wraps IndexedDB, WebSQL, or localStor),通过版本校验来确定是否需要更新语料,抑制请求浪费问题。
    2.   而网络问题🐦平台无法解决,我们永远无法确保请求始终 100% 可用,这就需要我们做好代码中的静态语料兜底,正如第 1 个问题所要解决的。
  3. 如何通过防御性编程保证语料展示?

    1.   我们在维护过程中其实还会有一种情况,那就是语料本身维护不全。譬如平台支持 18 种语言,每次新的需求提出新的文案,这 18 种语言文案并不一定能及时给到,如果直接渲染某一语种的语料,可能会出现文案缺失直接暴露 key 的情况。当然理想情况下应当保证语料 ready 再上线!

    2.   防御性地处理这个问题,首先需要我们设置兜底语言,正如第 1 个问题提到的,并保证兜底语言尽量全面。譬如我们保证英语文案一定全面。它不一定需要全部存储于代码中,我们可以在加载语料的时候,同时加载一份英语,并通过 merge 去更新,而不是直接替换,这样我们可以构建一条语料搜索链

    3.   远端对应语种 > 远端 en > 代码本地

    4.   这样基本可以保证我们不会直接暴露 key

富文本 / 插值

我们经常会需要在一段文案的中间,或是加粗字体,或是加超链接,或者别的什么逻辑。那么怎么把一个 text key 变成富文本呢?

这里就用到 插值(interpolate) 的概念。如 key = 'this is a {name}',这里大括号包裹的 name,便是个插值标识。

它并不单单服务 i18n 问题,任何文本组装的需求都适用插值方案。

插值概念在 python 中甚至默认存在

"{} {}".format("hello", "world")    # 不设置指定位置,按默认顺序
'hello world'
 
"{0} {1}".format("hello", "world")  # 设置指定位置
'hello world'

"{name}, {url}".format(name="tech", url="www.baidu.com")

在 js 中最常用、也是大部分库底层的是 formatjs ,它不仅仅提供 i18n 的插值方案,它是一个通用的插值替换工具。

不论是 i18next 还是 vueI18n 实际上都默认集成了插值工具,大部分情况下我们不用感知底层逻辑。

对于服务端渲染或者邮件生成,可以简单粗暴地把插值替换成 html,毕竟对于服务来说,不论怎样它就只是一堆 html 字符串,扔给客户端自己渲染就行。

而对于客户端 runtime(运行时),直接渲染 html 显然是一个很不安全的方案,这样渲染出来的 html 字符串必然需要执行,否则它不是真的会进入 dom 结构的 html 文本。

针对这个问题,react-i18next 和 Vuei18n 都给出了各自的方案。

(React i18n Trans 并不只有这种写法,但我推荐公司框架下使用这种,后文会提到)

React i18n 给的这种 demo 写法其实偏繁琐费解,文案中带 html tag 对于 Content Designer 来说是不合理的,我们有一个简化方案,在本文开头的 demo 里有这个简单的组件。

对于 CD 的体验来说,插值维护基本是和 Vuei18n 文案维护一致的,对于开发来说也进行了调用简化。

Vuei18n 集成的更简单易懂,都在 children 中

复数 Plural

国际化过程中还会遇到一种情况,有时我们需要根据量词,部分调整文案。可不同语言对于复数的处理差异很大。

比如对于中文来说:一只猫,两只猫,key = "{num} 只猫"。

对于英语:a cat, two cats, key1 = "{num} cat", key_other = "{num} cats"。

我们要在复数的情况下,给 cat 加个 s,如果只按上一节的方法,我们就要俩 key 来处理这个问题了!

更何况,有些语言中甚至不是按照单复数来调整描述的,甚至还有按照单双数来的,难道我们给每一个数字创建一个 key 再 ifelse 吗?


对于 VueI18n,内置 $tc 函数处理如下:

它识别一段文案里的 no、one 之类的标识来处理不同情况下的文案;


对于 i18next & React-I18next ,内置的处理如下:

它需要 count 变量 + 多个 key,key末尾拼接 _plural 之类的标识来处理。


对于 i18next,可以使用 i18next-icu Using with ICU format | react-i18next documentation 替换插值处理。

格式有一点类似 vue 。这样写更加清晰,第一个 numPersons 标识代表用于判断数量的变量,后面则是情况列举。虽然它依然会有点费解,但至少还是挺清晰的。

同时,🐦平台也以这种格式提供了单复数录入与输出格式。

注意区别

  1. i18next-icu 只能使用单大括号({xx}),vuei18n 默认使用单大括号,react-i18next 默认使用双大括号({{xx}})

  2. 使用 i18next-icu 插件,则不能用 组件的 children 格式写法。

SSR 服务

开源方案譬如 next-i18next,主要是在为自己的框架做包装,集成渲染方案,方便开箱即用。

SSR 根本上来说就是,服务先渲染一遍,生成预渲染的静态页面与预加载的数据,传输到客户端再无感执行 runtime 代码。

如果我们抛开框架自己开整,对于国际化来说,SSR 主要需要面对几个问题

  1. 在不同环境渲染时使用的 i18n 实例

    1.   next-i18next 几乎就是 react 解决这个问题得出的方案。不过 i18next 是兼容两种环境的,所以其实主要问题是控制渲染时用到的 i18n 实例,在单实例的场景下几乎是没啥问题的。
  2. 在不同环境文案的获取方式。

    1.   🐦平台的文案获取 sdk 可能在不同环境下是不能混合出现的,一般是由于其是否使用 window 对象,所以需要控制不同环境引用不同的包。
    2. 
        if (process.env.EDENX_TARGET === 'browser') {
          const pkg = await import('🐦平台.browser');
        } else {
          const pkg = await import('🐦平台.node');
        }
      
    3.   同时🐦平台远端接口不一定稳定,需要缓存与刷新机制,不可直接在渲染的时候调用。
    4.   在 node 渲染的同时,我们还需要把获取的语料送进数据预加载,在 client 侧的代码里再加载这些 预加载语料,防止 client 侧渲染导致文案变成 text key。
    5. 整个流程其实挺麻烦的,对于文案不频繁更新的业务,把文案存在 server 本地也是一种好方案。

  3. 在服务端渲染时的语言问题。

    1.   如果服务渲染频繁、无缓存、且并行线程进行渲染,那么一个 i18n 实例可能是不够的。

    2.   比如 A 请求一个 en 语言的页面,在渲染的过程中,服务并行接到一个 B 请求 zh 的页面,这时候如果只有一个实例,实例的语言被切成 zh,就会有可能导致 A 请求的页面中出现 zh 中文。

    3.   为了避免这个问题,在存在并行的业务中,建议是为每个语言配置一个 i18n 实例。

项目落地实践

国际化项目,代码的任何地方都有可能需要处理语料,项目大起来肯定是不能靠人力进行巡检管理的。

同时,在需求过程中,除非开发自己管理语料,我们很难期待文案总是处于先一步 ready 的状态。

在项目落地的过程中,我们需要一些工具进行自动扫描 or CI,或是确定语料的使用状况来进行语料库的垃圾清理。

静态扫描

🐦平台提供了工具来进行扫描和本地文案下载,同时有插件生态。

  • 我们可以在 lint-stage / husky 里面添加脚本,自动使用 CLI 对本地兜底文案更新。
"postinstall": "🐦平台 download -d ./local/all -l en"
  • 我们也基于🐦平台生态做了自动替换插件,简单来说就是结合业务代码,基于 diff 代码与字符串模式匹配扫描,根据远端语料进行代码自动替换。

  • 同理,利用 diff 代码 + 字符串匹配,我们也有 CI 工具扫描 key 是否已经在🐦平台上传。

Runtime 检查

I18next 内置了一些钩子,譬如 parseMissingKeyHandler 可以在 i18next 没有找到对应语料时回调。

I18n.init({
    parseMissingKeyHandler(key) {
      console.warn("missing key for [", key, "]");
      return key;
    },
});

我们也可以尝试去对用到的 key 回调,不过很困难。我们基本只能在 post processor plugins 生命周期内进行相关操作,可每个 processor 钩子只允许存在一个,我们需要的 processor 位置早就被其他格式化插件占了,譬如前文提到过的 i18next-icu。所以我们只能以一些 hack 的方式来获取。

Content Designer 合作

我们内部有一套标准流程规范,这套流程简单来说,复杂需求 CD 与设计师并行介入设计并确定文案 -> 产研生成 key-value 文档 -> CD 确认后提交给本地化专员 -> 最终文案上线。

在我们的流程中,我们与 CD 约定了文案格式,并与🐦平台团队合作,针对设计师使用的 figma 实现了一款插件,可以直接圈选元素,协助 CD 截图上传,方便开发与维护中的文案追踪。

它提前了流程中 “生成 key-value” 文档这一步,CD 在文案设计时可以顺手生成,在一定程度上解放了产研人力。同时因为在 figma 有一份文案管理,更加方便了 CD 寻找上下文更新文案。

样式 - RTL & Fonts

RTL 阅读顺序

有一些阿拉伯地区国家,使用从右向左的阅读习惯(RTL)。我们不可能为了这部分地区,单独开发新的页面样式。这就要求我们有一个方案来把我们从左向右 (LTR) 的默认样式改成 RTL。

RTL 方案的选择不多 postcss-rtlcss ,很多其他库都已经停止更新了。

它的基本的原理是在 html 上设置 [dir] tag (确保 native),再通过打包插件,生成一份针对 [dir] 的反转样式表。因此需要代码修改 dir 属性。

同时,有个别箭头图标或是别的什么也需要 rlt 翻转,这部分插件当然不会处理,需要自己个别控制。

const rootEl = document.documentElement;
rootEl.dir = isArab ? 'rtl' : 'ltr';

关于自带 RTL 内容的组件库 & 忽略

  1. 如果一个组件库自带 RTL 控制,组件库的样式需要手动 import,不需要 postcss-rtlcss 处理,那么 rtlcss 提供了方便的忽略方法:

编译如果用到 csso / css minify 在生产环境编译会把注释清掉,导致 ignore 不生效,解决方法在 /* xxx / 前面加感叹号,及 /! xxx */。

具体链接 github.com/css/csso#mi… comments 部分。

RTLCSS 有两种模式,默认模式中所有 css 会增加 [dir=ltr] / [dir=rtl] 前缀,为了保证页面初始样式正常,一定记得给 html 设置默认的 dir

另一种模式中,把样式拆分为默认样式和 [dir=rtl]前缀样式,这个有一个问题,它在拆分时会额外增加一些样式,最终导致样式不符合预期。

譬如,我们有 a { margin: 3px; },又有 a.div { margin-left: 12px; },在这种模式中,rtlcss 处理后会增加 [dir=rtl]a.div { margin-right: 12px; margin-left: 0 },导致原本另一边的 3px 被顶替。因此不推荐修改默认模式。

  1. 如果这个组件库用了一些非正常引入的方法,它自带了 rtl 处理,但它的 css import 完全不可控,我没办法加上 ignore,怎么办?

那我们只能各凭本事了。譬如我们用的一个组件库在编译代码里引入了样式文件,我没地方写 ignore,但我发现它是个 less,我发现 less 编译项可以加钩子,那么,用魔法打败魔法,我在钩子里给他加上了 ignore。

类似的,别的 css 品种总归有地方让你加上自己的自定义插件。

  1. 当然,我们总有可能遇到无法处理的情况,归根结底我们应该调整 css 的使用习惯,尽量使用逻辑样式,譬如 margin-left -> margin-start,让原生去解决这些问题。postcss-logical
原属性新属性(逻辑属性)原 className新 className
Margin
margin-leftmargin-inline-startml-XXms-XX
margin-rightmargin-inline-endmr-XXme-XX
Padding
padding-leftpadding-inline-startpl-XXps-XX
padding-rightpadding-inline-endpr-XXpe-XX
Position
leftinset-inline-start
rightinset-inline-end
TextAlign
leftstarttext-lefttext-start
rightendtext-righttext-end
Border
border-leftborder-inline-start
border-rightborder-inline-end

关于 javascript 生成 css

我们会遇到使用 javascript 来生成样式表的情况,如果这个 js 不是我们自己控制的,那么它肯定是不会被 postcss 处理的。

譬如 tailwindcss 如果是直接从 script 接入,那么接入的这部分是最原始的没有 rtl 部分的,只能使用 tailwind 官方指导的 [dir] 方式书写。

正常来说我们应当让它进入编译流程,但它严重影响编译性能。因此在不关注 rtl 的时候不用这么做,或者可以静态生成一份样式代替使用。

Fonts 字体

有时还会有需求,要求我们对某一些语言的字体进行替换。浏览器在渲染文字时,按照给出的 font-famliy 内容顺序,尽可能地去渲染每一个字符。

譬如某一字体 A 只有英语字母,它的下一级是系统字体 B,可以渲染大部分的文字。那么它在渲染法语时,对于法语中的英语字母使用 A 进行渲染,对于其中法语特有的字母使用 B 进行渲染,于是我们得到了一个 AB 混合的别扭单词。

为了应对这种特殊情况,首选肯定是换一个更全面地字体。如果不能满足这个要求,那么我们期望对不同语言使用不同字体。比较标准的方案就是在切换语言时,在 html 上设置 [lang] tag (确保 native),然后根据 [lang] 设置全局字体或 css 字体变量,并规范化项目中的字体使用。

这里的数字字符与中文被不同的字体渲染

其他业务落地问题 - Others

多语言图片 / 视频?

图片 / 视频处理的成本很高,图片中不出现文字最好。从前端视角来看,一般有两种方案处理这个问题:

  1. 根据语言直接切换图片 / 视频

    1.   这种方案最简单,也可以包括 server 渲染方案,主要方式就是根据 配置表 + 语言 切换展示对应内容。
    2.   对于 Banner 类的文字图片组合,业务方也可以开发相应模板,下发前端渲染。
  2. 前端根据某个模板,组合图片材料和文案进行渲染

    1.   airbnb.io/lottie/#/ 在某种程度上是可以实现需求的,不过它明显有些使用上的问题,譬如包体积大、上手困难、需要 Adobe AE 支持、RTL 适配不好等问题。

    2.   如果真的有相关需求,相对而言业务自己设计拼接模板、配置,可能会更好一些。

不同维度的文案?

我们遇到一些需要根据地区展示不同文案的需求,而不是根据当前语言。譬如我们要求 EU 地区不能显示 money 等关键字。针对这个问题我们也有几种方案:

  1. 从根源杜绝,不管什么地区大家都用一套文案规范;

  2. 利用 “复数” Plural 的特性来处理文案或嵌套翻译;

  3. 根据地区,在 runtime 直接修改语料包的 value 值;

    1.   我们由于一些各国法务原因,导致各地区没办法用一套规范,为了尽量做到 CD、研发无感,最终采取了这套方案。
  4. Hack 翻译框架(不推荐)

外链?

一般来讲每个网站都应有多语言的识别方式,我们只要适配即可。

  • 拼接 url query,如 lang=xx

  • Cookie,如 cookie 中的 lang

邮件 & 通知?

邮件、通知一般从 server 侧或者某些平台下发,server 侧一般会需要对邮件模板或通知模板中的文案插值进行多语言填充。

关于邮件模板的生成,开源已经有很多优秀方案,不在本文的讨论范围内了。