出海项目之多语言国际化

975 阅读5分钟

去年(2023年)启动了出海项目,在项目的过程中,遇到了不少做国内项目没遇到过的问题。

背景

项目中的所有页面都需要支持不同语言(汉语zh、英语en、阿语ar)的文案和对应的UI布局,需要找到更简单更通用的解决方案。

与国内项目比较,最主要的差异点有两个:

  1. 多语言文本展示
  2. UI排版布局差异

解决方案

多语言文本展示

经过调研,业界广泛使用的i18n方案是 i18next ,基于使用React技术栈的考虑,使用 react-i18next 库。

react-i18next 官网

语言展示逻辑

基于业务规划,包括有APP和浏览器访问的页面,汇总出如下3个方案:

  • URL参数方案: 与客户端约定,打开webview的url上拼接lang=xx的指定语言参数。前端优先通过页面url上的lang参数获取用户当前语言。
  • APP接口方案: 前端调用客户端提供的语言接口,获取用户当前语言。
  • 浏览器/Webview接口方案: 前端调用浏览器api,获取用户当前语言。

首先,优先使用URL参数约定的方式;其次根据访问的环境(APP/外部浏览器),逐步降级使用APP接口Web接口

多语言文本

因为需要支持不同的语言,可以把展示的文本都汇总起来,定义各个文本的key,并根据不同的语言分别定义在json文件,文件命名格式为xxx_en.json / xxx_zh.json 等。

例如login_en.json包括的部分内容如下:

{
	"login_terms_msg": "Agree to the agreements and terms before you can login ",
	"login_sign": "Sign up",
	"login_save": "Save",
	"login_apple": "Sign in with Apple"
}

渲染方式

目前只需要CSR方案,使用上述提到的react-i18next开源库进行多语言文案展示。后续需要使用SSR方案的话,也同样适用,但需要做少量的差异化的改动。

UI排版布局差异

由于不同国家地区的阅读习惯方向不同,需要针对不同的语言,指定对应的布局(LTR、RTL)进行展示,同时尽可能做到减少额外的开发。

世界上绝大多数国家地区的主流阅读习惯都是LTR(从左到右),少数国家地区(阿拉伯语、希伯来语等)是RTL(从右到左),相应地,我们也需要专门针对这些语言的页面,把整体的UI排版布局做镜像调整。

候选方案:

  • 方案一:CSS的direction属性
    • 优势:简单粗暴
    • 不足:非对称布局以及其他稍微复杂的布局无效,间隔边框等无效,都需要额外的代码进行适配
  • 方案二:rtlcss开源库
    • 优势:能支持间隔边框等指定方向的CSS属性的生成替换,能自定义属性和内容的生成替换
    • 不足:需要工作流额外处理,会生成两份样式代码,内联样式无法生成替换
  • 方案三:CSS的transform属性
    • 优势:写样式可以完全不用关注RTL布局
    • 不足:包含文案、图片等内容需要避免镜像反转的组件需要用翻转组件包裹,transform在移动端可能存在奇怪的兼容问题

最终,考虑到方案一/三都覆盖得不够全面,需要额外处理较多,选择了使用rtlcss的方案二。

选择使用 postcss-rtl 的npm包,它的具体的配置和运行原理如下:

项目配置

  • npm包引入
  • webpack配置引入postcss-rtl插件

代码编译

webpack构建时执行插件,postcss-rtl插件对css代码中的涉及水平(左右)方向的属性进行rtl的补充修改并输出到css文件。

包括但不限于以下属性:

  • border属性集
  • padding属性集
  • margin属性集
  • direction
  • float
  • left
  • right
  • text-align
  • transform属性集

代码举例:

scss源码

.box {
  display: flex;
  justify-content: space-between;
  .onleft {
    padding-left: 10px;
  }
  .onright {
    padding-right: 15px;
  }
}

编译输出css代码

.box {
	display: flex;
	justify-content: space-between
}

[dir=ltr] .box .onleft {
	padding-left: 10px
}

[dir=rtl] .box .onleft {
	padding-right: 10px
}

[dir=ltr] .box .onright {
	padding-right: 15px
}

[dir=rtl] .box .onright {
	padding-left: 15px
}

更多内容参考 rtlcss官方

运行时

需要在DOM根元素,手动添加针对不同语言对应的lang和dir属性

出海项目之多语言国际化-1.png

执行代码

/i18n/resources.js

获取 assets/static/locales/ 下的多语言文本。这里遇到一个问题,是 require.context 获取到的文件内容对象,同一个文件会获取到两次,对应的key分别是绝对路径和相对路径,因此需要过滤掉其中之一。

const langList = ['ar', 'en', 'zh']
const resourcesObj = {}

function importAll(ctx) {
  const obj = {}
  ctx.keys().map(item => {
    if (item.indexOf('./') < 0) {
      return
    }
    obj[item.replace('./', '')] = ctx(item)
  })
  return obj
}

const overall = importAll(require.context('assets/static/locales/', false, /^\.\/([a-zA-Z0-9_]+_)?[a-z]{2}\.json$/))

langList.forEach(lang => {
  resourcesObj[lang] = {
    translation: {}
  }
  Object.keys(overall).map(item => {
    if ((new RegExp(`${lang}\.json$`)).test(item)) {
      Object.assign(resourcesObj[lang].translation, overall[item])
    }
  })
})

export const resources = resourcesObj

/i18n/index.ts

获取当前语言,写入对应的布局相关设置,以及传入多语言文本对i18next进行初始化。

import i18n, { Resource } from 'i18next'
import { initReactI18next } from 'react-i18next'
import { getCurrentLang } from '../mobileApi'
import { getURLParam } from '../utils'
import { resources } from './resources'

const lang: string = getURLParam('lang') || ''
let langFromApp = ''

const RTL_LANG = ['ar', 'he']

const getLang = async (): Promise<void> => {
  const res = await getCurrentLang()
  langFromApp =  (res && typeof res === 'string') ? res : 'en'
  i18n.changeLanguage(langFromApp)
  updateDirection(langFromApp)
}

export function updateDirection (lang: string) {
  if (!lang && !langFromApp) getLang()
  const dir = RTL_LANG.indexOf(lang || langFromApp) >= 0 ? 'rtl': 'ltr'
  window.document.documentElement.lang = lang || langFromApp || 'en'
  window.document.documentElement.dir = dir
  return dir
}

export function changeLanguage (lang: string) {
  i18n.changeLanguage(lang, () => {
    updateDirection(lang)
  })
}

i18n.use(initReactI18next)
  .init({
    debug: !__PROD__,
    lng: lang,
    fallbackLng: 'en',
    resources: <Resource>resources,
    interpolation: {
      escapeValue: false	
    }
  })

updateDirection(lang)

export default i18n

最后别忘了,在webpack config里面,添加 postcss-rtl 相关的配置。