功能点

前端国际化与本地化

2021-02-24  本文已影响0人  犯迷糊的小羊

背景

在信息技术领域,国际化与本地化(internationalization and localization)是指修改软件使之能使用目标市场的的语言、地区差异以及技术需要;

具体来说,国际化是在设计软件,将软件与特定语言及地区脱钩的进程,当软件被移植到不同的语言及地区时,软件内部不用改变或修正;本地化则是当移植软件时,加上与特定区域设置有关的信息和翻译文件的进程;

国际化工作概述

软件国际化与本地化的实现主要涉及以下部分:

本次国际化方案将只针对前端国际化展开叙述,并采用 React UI 框架;

技术方案

目前,主流的国际化解决方案有基于 GNU gettext 的软件包以及基于 CLDR 标准的 ICU 函数库;

GNU gettext

GNU gettext 是 GNU 国际化与本地化函数库,常被用于编写多语言程序,node-gettext 是 gettext 在 JavaScript 语言中的实现;

CLDR 标准

Unicode CLDR 为软件提供了支持世界语言的关键构建模块,提供了最大,最广泛的语言环境数据库。

这些数据被广泛的公司用于其软件国际化和本地化,使软件适应不同语言的惯例以用于此类常见软件任务。

ICU 函数库

ICU 有一套自定义的国际化语法规范,不同的语言有各自的类库实现,在 JavaScript 中有 messageformat

方案评估

本次国际化候选输出方案有两套,react-intl 和 node-gettext + react-gettext-parser + narp(可选);

react-intl

react-intl 是 yahoo 推出的基于 FormatJS 的 react 应用的国际化方案,FormatJS 的核心库是 Intl MessageFormat,遵循的是上述的 ICU 语法规范;

其基本原理是维护几份不同语言包的映射表,然后通过设置当前应用的语言动态的选择不同的语言包,应用内部组件根据语言包的映射表的 id 找到对应的特定语言版本词条,从而实现国际化,具体实现流程可参考:react-intl 实现 React 国际化多语言

react-intl 的方案优点在于:

其缺点在于:

node-gettext + react-gettext-parser + narp

本套方案是沿革至 GNU gettext 的翻译工作流,结合上述类库,其实现步骤如下:

1. 使用 node-gettext 库提供的相关方法,对源码进行翻译标记

这里为了减少代码量,对 gettext 进行封装为 "_" 等形式;

// src/pages/Index/index.tsx
import * as React from 'react';
import {_, _p} from 'utils/gettext';
export default class Index extends React {
  render() {
    return (
      <div className='index'>
        <h3>
          {_('你好,悦跑圈')}
        </h3>
        <p>
          {_p('一个苹果', '%d 个苹果', 4)}
        </p>
        <p>
          {_('姓名:%s', 'teren')}
        </p>
      </div>
    )
  }
}

由于 node-gettext 不支持插值,所以结合 sprintf-js 实现插值输入功能;

// src/utils/gettext.ts
import Gettext from 'node-gettext';
import {sprintf, vsprintf} from 'sprintf-js';

const gt: Gettext = new Gettext();

export function _(msgid: string, value?: IValue): string {
  const str = gt.gettext(msgid);

  return (
    value
    ? value instanceof Array
        ? vsprintf(str, value)
        : sprintf(str, value)
    : str
  );
}

2. 使用 react-gettext-parser 工具库提取源码中的标记信息,生成 pot (portable object template)文件

$ react-gettext-parser --output messages.pot 'src/**/{*.js,*.jsx,*.ts,*.tsx}' '!src/test.js'

提取出来的 pot 文件如下:

#: src/pages/Index/index.tsx:26
msgid "姓名"
msgstr ""

msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: joyrun-match-enrollment\n"
"Language: zh-CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#: src/pages/Index/index.tsx:23
msgid "一个苹果"
msgid_plural "%d 个苹果"
msgstr[0] ""


#: src/pages/Index/index.tsx:20
msgid "你好,悦跑圈"
msgstr ""

#: src/pages/Index/index.tsx:26
msgid "姓名:%s"
msgstr ""

3. 将 pot 文件交由翻译者,翻译者使用翻译工具(如 poedit 等)将 pot 文件导入后逐条翻译

将翻译后的文件保存为对应的 po 文件,如 en.po 和 zh_Hant.po,再调用 gettext-parser 将其转换为对应的 json 文件;

const fs = require('fs');
const input = fs.readFileSync('en.po');
const po = gettextParser.po.parse(input);
fs.writeFileSync('en.json', po);

[注意] 对于后续新增的翻译词条,需要使用 pot-merge 将 pot 文件进行合并操作

$ node pot-merge.js -a message.pot -b en.po -o en.po

4. 将翻译好的语言包引入代码中

import Gettext from 'node-gettext'
import enTrans from './en.json'

const gt = new Gettext()
gt.addTranslations('en', 'messages', enTrans)
gt.setLocale('en')

gt.gettext('你好,悦跑圈')
// -> "Hello, Joyrun"

上述的第 2 和 第 3 步骤如果没有一套工具链配合,实际操作起来相对繁琐,所幸社区提供一个工作流工具 narp 简化工作流;

narp 提供 push 和 pull 两个命令;

push 操作先通过 react-gettext-parser 从源码中提取待翻译的字符串,形成中间 pot 文件,然后通过 pot-merge 合并从上游翻译服务器的和本地的翻译文件,最后将合并后的新的 pot 文件上传至翻译服务器(Transifex or POEditor);

pull 操作则是从上游翻译服务器下载翻译好的 po 文件,然后通过 gettext-parser 将 po 文件转换为 json 文件并写入磁盘;

POEditor POEditor

node-gettext + narp 的方案的优势在于:

其劣势在于:

[注意] 关于 po、pot 和 mo 文件的区别详见此文

结合本次赛事报名项目来看,本次项目采用 node-gettext + narp,原因是考虑其代码的良好维护性和简洁性、以及国际化工作的协作性。

其他

翻译词条

参考资料

上一篇 下一篇

猜你喜欢

热点阅读