软件工程中的国际化(i18n)和本地化(l10n)

本篇文章主要用于介绍软件开发过中对于locale, 字符集等作用和使用方法,让软件更好的适应全球不同地区的工作。

国际化和本地化

一、背景介绍

“国际化和本地化”是指信息技术领域中,修改软件使之能适应不同目标市场带来的地区差异的过程,地区差异主要指语言,习惯,文化等等。

众所周知,计算机是由美国人发明的,在早期软件信息交互使用英语来完成没有问题的,但随着时代的发展,同一款软件会被全球很多地区的用户使用,而这些用户的语言,习惯,文化等又有差异,如果想让用户和软件的交互没有障碍,就须针对不同地区用户给出相应解决方案,这样就诞生了“国际化和本地化”这个概念。

国际化,Internationalization, 行业习惯简称i18n。通过使用国际标准或者提取资源的方式,让软件和具体区域脱钩。i18n提供的是多语言支持框架,也意味着产品适用于任何地方的“潜力”,而且i18n只需要做一次。

本地化,Localization, 简称L10n, 在i18n的基础上,针对区域差异进行个性化配置,让产品可以在特定区域进行使用。L10n提供的是语言和地区的支持。L10n需要根据不同的区域各做一次。

Miscrosoft和IBM习惯使用“全球化”来表示“国际化”和“本地化”两个过程的,Globalization, 简称g11n;也习惯使用GILT(Glbalization, Internationalization, Localization, Translation)来表示。

二、工作内容

在i18n和L10n过程中,常见的工作内容有:

  1. 语言类
    1. 字符,比如英语主要是A,B,C…拉丁字母,而中文是汉字。
    2. 数字,比如有阿拉伯数字1、2、3…,还有罗马数字I,II,III…
    3. 方向,比如阿拉伯人和希伯来人的阅读习惯是从右向左。
    4. 大小写,比如大多非拉丁字母没有大小写的概念
  2. 文化类
    1. 图片和颜色,比如国内股市和美国股市的颜色相反
    2. 名字和称谓,据统计汉语有58个个亲属称谓,而英语只有23个。
    3. 电话号码&地址&邮编,比如中国地址都是区域从大到小,而美国是从小到大
    4. 货币,比如货币符号,货币格式等
    5. 度量衡,比如英尺,英镑等
  3. 书写习惯类
    1. 日期和格式,比如年/月/日,还是日/月/年
    2. 排序,比如习惯从大到小,还是从小到大

其他的还有翻译工作,包括界面上的文字翻译和使用手册的翻译,主要是多语言处理工作。

将文本和环境相关的资源与程序代码隔离开是编写国际化软件的常用做法,当使用环境发生变化时,不需要修改代码,只要修改资源就可以了,这样简化了本地化的工作。

三、字符和编码

文字的出现标志这人类进入了文明时代,最早是通过直接书写字符的方式来传递信息,随着通信技术的发展如何将字符进行数字化成为首先要解决的问题,我们在战争片中经常可以看到电报员通过“滴滴滴…”的操作发送信息,其实就是字符的数字化的信息-摩尔斯电码,它通过不同的排列顺序来表达不同的英文字母、数字和标点符号。

在计算机领域,我们把一个系统支持的所有抽象字符的集合称为字符集,也就是Character Set, 简称CS。字符集是各种文字和符号的总称,包括各个国家的文字,标点符号,图形符号以及数字等。

字符编码模型

为了进行数字化处理应用,必须把字符集中的字符编码为指定集合中的一个对象(例如,比特模式,自然队列,8位组,电脉冲),这样才能在计算机中进行存储和通过通信网络进行传输,我们把编码出来的集合叫做字符编码集,Character Encoding Set, 简称CES。这就是简易编码模型。

为了高效的完成全球的字符编码工作,现代字符编码模型并没有采用简易的编码模型,而是自下而上分为以下五个层次:

  1. 抽象字符集,Abstract Character Repertoire,ACR, 是一个系统所有抽象字符的集合。抽象字符是指代表某个字符,不表示字形,字形的工作由计算机文字显示模块进行处理。

  2. 编码字符集,Coded Character Set, CCS,抽象字符集中的每个字符映射为一个可以数字化的位置,可以使用坐标或者非负整数来表示,我们称之为CodePoint码位。而所有码位的集合就是编码空间,比如GB2312的汉字编码空间为94x94。

  3. 字符编码表,Character Encoding Form, CEF, 我们已经有了每个字符的码位, 但是想用计算机处理就必须按照计算机的处理单元来进行转换编码,也就是将编码字符集中的码位转换为有限比特长度的整型值序列,也叫CodeUnit,码元。转换编码可以分为定长编码(比如UTF-8)和变长编码(比如UTF-16)。

  4. 字符编码方案,Character Encoding Scheme, CES, 对于CEF中的码元如何在文件中进行存储,存在顺序问题,比如一个两字节的UTF-16的编码字符0xAABB, 是存放为0xAA, 0xBB, 还是0xBB,0xAA,所有就需要规定编码方案,比如UTF-8(无字节序), UTF-16LE(小端), UTF-16BE(大端)

  5. 传输编码语法,Transfer Encoding Syntax, 用于处理上一层字符编码方案提供的字节序列,比如Base64把8位字节编码为7位长的数据,LZW可以进行无损的压缩。

常见字符标准介绍

  • ASCII
    • American Standard Code Information Interchange, 美国信息交换标准代码
    • 7bit, 包含94个可打印字符,就是英文大小写字符,阿拉伯数字以及西文符号
    • EASCII是ASCII的扩展版本,使用8bit
  • ISO-8859
    • 同时包含字符集和编码集
    • 拥有15个字符集
    • 不包含中日韩文字
    • 1982年发布
  • GB2312
    • 中华人民共和国国家标准简体中文字符集
    • 1981年发布
    • 收录6763个汉字,覆盖中国大陆99.75%的使用频率
  • ISO-10646和Unicode
    • ISO-10646是由ISO/IEC组织制定,其中包含的标准字符集被成为通用字符集,Universal Character Set, UCS
    • Unicode是由统一码联盟制定,后来和ISO/IEC协商,从Unicode2.0开始采用相同的字库和字码,两个组织发布各自标准,但保持标准兼容
    • 1991年,Unicode1.0发布,不包含CJK(中日韩统一汉字集)
    • 1993年,Unicode1.1发布,包含CJK
    • Unicode规定了字符编码表和编码方案,由UTF-8,UTF-16,UTF-32等等;ISO-10646则规定了UCS-2,UCS-4
    • Unicode自版本2.0开始保持了向后兼容,即新的版本仅仅增加字符,原有字符不会被删除或更名。
    • 最新的Unicode版本为12.1
  • GBK
    • 汉字内码扩展规范
    • GBK是对GB2312的扩展,收录21886个汉字和图形符号
    • 只为“技术规范指导性文件”,不属于国家标准,国家质量技术监督局于2000年3月17日推出了GB 18030-2000标准
  • GB18030
    • 中华人民共和国国家标准所规定的变长多字节字符集
    • 与GB2312完全向后兼容,与GBK基本向后兼容
    • 共收录汉字70,244个
  • BIG5
    • 繁体中文字符集标准
    • 共收录13,060个汉字

四、Locale技术(LINUX)

通过上面我们知道,想要使用i18n, 就必须提供一种技术来完成这个工作,于是就有了Locale。

Locale,也称为“本地化策略”,至少会包含语言和地区,包括数据格式、货币金额格式、小数点符号、千分位符号、度量衡单位、通货符号、日期写法、日历类型、文字排序、姓名格式、地址等等。

在Linux下通过环境变量来配置区域,这些环境变量会直接影响libc函数和相关命令的输出信息内容和格式,比如

1
2
3
4
5
# LC_ALL=en_US.utf8 date
Tue Jul  9 16:16:58 CST 2019

# LC_ALL=zh_CN.utf8  date
2019年 07月 09日 星期二 16:17:08 CST

使用Locale技术相比Hard-coding带来的好处有:

  • 软件可以进行区域无关的代码实现
  • 软件可以提供一致的区域化配置接口
  • 操作系统区域显示优化可以直接体现在软件上

环境变量

常用的Locale环境变量有以下六个:

  1. LC_CTYPE, 编码相关,对于isslower(), mblen(), mbstowcs()的函数行为有影响
  2. LC_COLLATE, 排序相关,对于strcoll(), strxfrm()的函数行为有影响
  3. LC_MESSAGES,输出表达相关,比如表达是/否等,对于gettext(), ngettext(), rpmatch()等函数行为有影响
  4. LC_MONETARY, 货币显示相关,对于localconv()等函数行为有影响
  5. LC_NUMBERIC, 数字格式有关,对于printf(), scanf(), strtod()等函数行为有影响
  6. LC_TIME,时间日期相关,对于strftime(), strptime()等函数行为有影响

除了针对具体相关的环境变量LC_XXX,还有两个特殊的环境变量,一个是LC_ALL,另外一个是LANG,它们使用的优先级是这样的:

1
首先查看LC_ALL是否配置,如果就使用LC_ALL,如果没有就使用相应的LC_XXX,如果LC_XXX没有配置,就使用LANG

因为Locale的环境变量会影响到函数的行为,所以一般开源软件都会设置LC_ALL=C, 表示清除本地区域设置,这样可以保证软件的行为一致。

区域配置查看

查看当前使用的区域配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# locale 
LANG=zh_CN.UTF-8
LANGUAGE=zh_CN:zh
LC_CTYPE=zh_CN.UTF-8
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_PAPER="zh_CN.UTF-8"
LC_NAME="zh_CN.UTF-8"
LC_ADDRESS="zh_CN.UTF-8"
LC_TELEPHONE="zh_CN.UTF-8"
LC_MEASUREMENT="zh_CN.UTF-8"
LC_IDENTIFICATION="zh_CN.UTF-8"
LC_ALL=

查看所有支持的区域配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# locale -a
C
C.UTF-8
en_AG
en_AG.utf8
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_IN.utf8
en_NG
en_NG.utf8
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZM
en_ZM.utf8
en_ZW.utf8
POSIX
zh_CN.utf8
zh_SG.utf8

区域配置命名规则

规则:

1
语言[_区域][.字符集][@修饰]

解释: * 规则:语言[_区域][.字符集][@修饰] * 语言:两个小写字母,需要符合ISO639 * 区域:两个小写字母,需要符合ISO3166 * 对于”字符集”和“修饰”没有标准要求

如果需要知道系统支持哪些字符集,可以使用下面命令来查看

1
locale -m

五、程序中字符串

“Talk is cheap, show me the code” –Linus Torvalds

让我们来看看代码中的字符串的转换过程, 首先有以下C源码,我们这里假定编码格式为UTF-8,所以如果想得到相同的结果请无比确保这一点。

#include <stdio.h>
#include <string.h>
#incluide <wchar.h>

int main(int argc, const char* argv)
{
    const char* t1 = "Hello,World";
    const char* t2 = "你好,世界";
    const wchar_t* t3 = L"Hello,World";
    const wchar_t* t4 = L"你好,世界";

    return 0;
}

对于该C文件,所有字符都使用了UTF-8的格式来进行存储,每个字符串对应的UTF-8编码字节序列如下:

Hello World ==> 0x48 0x65 0x6c 0x6c 0x6f 0x2c 0x57 0x6f 0x72 0x6c 0x64
你好,世界  ==> 0xe4 0xbd 0xa0 0xe5	0xa5 0xbd 0xef 0xbc 0x8c 0xe4 0xb8 0x96	0xe7 0x95 0x8c

那么,经过GCC编译器生成二进制以后,程序在运行过程中,每个变量存储的值是什么呢?利用gbd工具可以查看

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) x/16b t1
0x400620:	0x48	0x65	0x6c	0x6c	0x6f	0x2c	0x57	0x6f
0x400628:	0x72	0x6c	0x64	0x00	0xe4	0xbd	0xa0	0xe5
(gdb) x/16b t2
0x40062c:	0xe4	0xbd	0xa0	0xe5	0xa5	0xbd	0xef	0xbc
0x400634:	0x8c	0xe4	0xb8	0x96	0xe7	0x95	0x8c	0x00
(gdb) x/16b t3
0x40063c:	0x48	0x00	0x00	0x00	0x65	0x00	0x00	0x00
0x400644:	0x6c	0x00	0x00	0x00	0x6c	0x00	0x00	0x00
(gdb) x/16b t4
0x40066c:	0x60	0x4f	0x00	0x00	0x7d	0x59	0x00	0x00
0x400674:	0x0c	0xff	0x00	0x00	0x16	0x4e	0x00	0x00

我们可以看到,t1和t2在运行时的二进制序列和C源码文件中存储的相同(UTF-8编码),但是t3和t4则固定使用四个字节来表示一个字符,进一步发现都使用UTF-32编码,也就是说如果使用L这样的前缀,编译器就会自动把字符串按照UTF-32的编码格式转换。

那为什么会有wchar_t这个类型呢?

wchar_t我们称之为宽字符,当一个字节无法满足我们存储系统支持的字符的时候,通过多个字节来存储系统字符就自然而然的事情,对于这个每个编译器的选择不同,Linux使用四个字节来存储UTF-32来存储字符,而win32 MSVC则使用两个字节来存储UTF-16编码。对于常量字符串我们需要通过‘L’前缀来告诉编译器我们需要使用宽字符来进行处理。

那什么又是多字节字符呢?最早的时候只处理ASCII字符,后来随着字符编码集的发展,一个字符可能对应不同的字节数量,对于t2来说就是个多字节字符。

既然有宽字符wchar_t, 也自然也就有相应的函数来完成宽字符的字符串操作,比如wcslen()对应strlen(), wcscpy()对应与strcpy(), 所以当我们需要获取“世界,你好”这种字符串的长度的时候,就需要使用wchar_t和wcslen()才能正确得到“5”这个正确值。

六、常用工具

iconv & libiconv

虽然有了Unicode这个大一统的标准,但是已经存在的编码方式早已蔓延在各个领域,所以字符编码的转换也是常见的工作,iconv这个命令可以完成编码转换的事情。

1
iconv [OPTION...] [-f encoding] [-t encoding] [inputfile ...]

默认是输出到标准输出终端的,可以使用’>’重定向保存输出到文件中。iconv几乎覆盖了所有的字符编码集的支持。

如果需要编程开发,则使用libiconv库的api来完成

#include <iconv.h>
iconv_t iconv_open (const char* tocode, const char* fromcode);
size_t iconv (iconv_t cd,
                     char **restrict inbuf, size_t *restrict inbytesleft,
                     char **restrict outbuf, size_t *restrict outbytesleft);
int iconv_close (iconv_t cd);

七、术语对照和参考文章

  • 抽象字符, Abstract character
  • 码点,Code Point
  • 码元,Code Unit
  • 码点空间,Code Space
  • 美国标准编码信息交换,ASCII, American Standard Code Information Interchange
  • 国际电工委员会,IEC, International Electrotechnical Commission
  • 国际标准化组织,ISO,International Organization for Standardization
  • 统一码联盟,Unicode Consortium
  • 统一码标准,The Unicode Standard