一个成功的产品要走向全球需要经历很多环节,从软件开发的视角主要有国际化和本地化两个流程:

语言环境是在某个国家或地域内使用特定的语言或语言变体,其决定了日期、时间、数字和货币的格式和解析方式,以及各种测量单位和时区、语言、国家与地区的翻译名称。国际化使一个软件能够处理多个语言环境,本地化使一个软件支持一个特定的地区的语言环境。这意味着全球化的流程是先使软件具备国际化的能力,之后做本地化实施使其能支持特定地区特定的语言环境。
基于他们的英文单字长度过长,常被分别简称成i18n(18意味着在“internationalization”这个单字中,i和n之间有18个字母)及L10n。使用大写的L以利区分i18n中的i和易于分辨小写l与1。(Wikipedia)
我们知道国际化是为了解决与用户本地语言相关的文本显示与输入的问题,这个问题又与用户国家和语言相关,比如同样的英语在美国和英国就不同。在国际化标准还未出现之前,曾经有多种表示国家与语言的方法,这个 Making Sense of Language Tags 的 Slide 就分享了这段有趣的国际化标准问世的历史。直到 IETF BCP(Best Current Practice) 47 的出现,才统一规定了国际化中语言标识(Language Tag)的定义及匹配标准。
而由于很多软件与系统早于此标准出现,就会出现一些与此标准不统一的问题。一个突出的问题就是语言标识定义中的连接线的选择,在 Linux 系列的操作系统中用 locale 来定义语言环境,如en_US表示美国英语,而在 BCP 47 中用en-US表示美国英语。前者选择了用_而后者采用了-来连接语言和国家。这种混乱有时候会带来很多意想不到的困惑,有时候你使用的某个库支持en_US,有的库却支持en-US,这不得不让国际化实现的过程中多了一些兼容性处理的工作,甚至因为语言不统一,出现很多沟通上的问题。
国际化相关的标准如下:
一个完整的语言标识(Language Tag)组成如下:

更详细的介绍见我这个 i18N in Java 的 Slide。
不同编码有着可以表示不同字符集合的区别,比如我们无法用 ASCII 编码来表示汉字。Unicode 字符集可以用从 0 到 10FFFF (十六进制)范围的码点来显示几乎所有人类已知的字符。它的存储至少需要 21 位。文本编码系统 UTF-8 将 Unicode 码点适配到一个合理的 8 位数据流,并兼容 ASCII 数据处理系统。UTF 表示 Unicode 转换格式(Unicode Transformation Format)。
自 2009 年以来,UTF-8 一直是万维网的最主要的编码形式。截止到 2019 年 11 月, 在所有网页中,UTF-8 编码应用率高达 94.3%(其中一些仅是 ASCII 编码,因为它是 UTF-8 的子集),而在排名最高的 1000 个网页中占 96%。所以在国际化中推荐采用 UTF-8 编码。
这篇 IT产品的国际化,绝不是“支持英文”就足够 文章提到一些 GBK 编码的文本中有许多“看起来一样”的文字,其实有细微差别。但是,为了节省 Unicode 中的空间,给它们指定了同样的 Code Point。

如何区分这些同样码位(用不同字形显示一个字符,即同一字位)的同位异字?这就需要 locale 的帮助了。
计算汉字数量时,通常是按照字形来计算的,即将一个代表相同语音语义的字的简化,繁体,异体,新字形,旧字形等等分别进行计算。这种计算方式实为是在计算变体。所以,长期以来错误地把大型字典里收入的字形数看作是汉字系统的规模。(Wikipedia)
locale 是软件在运行时的语言环境, 它包括语言(Language), 地域(Territory)和字符集(Codeset)。locale 使用 language tag 标识语言国家,比如在 GNU Linux 中的定义格式为: 语言[_地域[.字符集]],如美国英文是en_US.UTF8。在 Linux 中 locale 包括以下几个部分:
如果你的 locale 是 en_US.UTF8,那么必须将其修改为 zh_CN.UTF8 才能正确显示中文。在 macOS 操作系统的 /usr/share/locale 目录中存放着全部支持的 locale:

而在 BCP 47 中,language tag 的定义为langtag = language["-" script]["-" region]*("-" variant)*("-" extension)["-" privateuse]。
同一种语言在不同国家地区可能有一些细微的差异,比如美国英语和英国英语就有一些差异。同一个国家可能也有多种语言,比如中国有简体与繁体语言。在上述 locale 的介绍我们看到了使用语言_地域或语言-地域的方式来确切的表达一个国家的语言。
对于国家和语言 ISO 制定了相应的标准代码:ISO 3166-1 与 ISO 639-1。
浏览器使用语言代码来在 Accept-Language HTTP 头里发送浏览器接受的语言名。比如:it, de-at, es, pt-br 。
GNU gettext 是 GNU 国际化与本地化(i18n)函数库,它常被用于编写多语言(multilingualization,缩写为 M17N)程序。许多编程语言如 C、C++、Python、PHP、Rust、Elixir 等都在语言内部支持了 gettext 的使用。
以下是 Java 调用 gettext 完成国际化的流程:

ResourceBundle 类的 Java 类文件。如下图是 PHP 使用 gettext 实现国际化的流程图:

Elixir 使用 gettext 实现 i18n 的目录结构:
priv/gettext
└─ en_US
| └─ LC_MESSAGES
| ├─ default.po
| └─ errors.po
└─ it
└─ LC_MESSAGES
├─ default.po
└─ errors.po
gettext 的使用流程就是一个典型的使应用支持 i18n 国际化的过程:

如上图是一个典型的本地化流程图。其中参与方有:
这块首先要考虑这些基本的前置问题:
语言_地域或语言-地域格式,如en-US代表美国英语语言。
本地化的挑战主要有不同地域的语言、文化、书写习惯及法律方面的差异带来的问题,具体有以下类别:
如果是 toC 的网站做本地化,需要考虑一些和搜索引擎优化 (SEO) 的事情,如这篇 How to approach an international strategy 提到的一些关键点:
www.mysite.com/de/ 会告诉用户页面是德语的;hreflang元标记向 Google 显示您要定位的语言。如<link rel="alternate" href="http://example.com" hreflang="en-us" />;同一内容在不同地域使用更贴合本地内容的设计会带来更好的效果,如 产品设计的国际化与本地化 这篇文章中提到 Spotify 的歌单封面在不同国家的差异呈现形式:

从架构的角度看单体应用的本地化流程比较简单。但现在很多应用都是微服务架构,多个团队协作开发的模式。如果是各自团队负责各自服务的本地化,必须有统一的本地化委员会制定本地化技术标准:
或者有专门的本地化团队实施本地化,前面这些问题将由这个团队负责解决。笔者参与的项目就属于后者,我们的团队完成整个大系统近十几套微服务子系统的本地化,这十几套系统又由几个大组多个团队负责,这类跨功能需求(CFR)在多个团队中的协作流程是个复杂的工作。
在实施本地化之前,确定相关技术或业务标准是重要的事情,一些技术或业务标准有:
en.json存放英语的静态文本,而en_US.json存放和美国英语相关的文本(如计量单位、日期、数字和货币等);语言-地域格式,如en-US代表获取美国地区英语语言的本地化版本。实际上我们团队在做本地化最耗费时间的就是本地环境的启动。因为涉及的服务众多,不同服务启动的方式又有着细微的差异,甚至指导文档也是错误的,需要不断的踩坑才能完成环境的搭建。最终我们的处理方式是联系各开发团队,每次在做某个服务的本地化前期,会找开发团队帮助我们设置本地的环境。
另外一个难点在于我们对业务的不了解。由于每个服务都有大量的组件和页面,包括后端服务不同来源的动态数据,光靠我们自己摸索很难搞清楚。最终我们在做这个服务的本地化前期,会找开发团队的业务分析师帮助我们介绍这个服务涉及的业务流程。
一些国际化站点的语言或地区切换设计成超链接,用户可以通过链接访问不同语言地区版本的站点,这类站点并不需要存储语言或地区的配置。
具备用户个人信息配置的站点一般会提供在个人信息设置中设置偏好语言和地区,这样用户在切换设备的时候可以同步上次设置的语言或地区。
如果你的站点用户切换设备并不频繁,简单的处理可以将这些配置存入浏览器存储中。当用户切换设备后,自动恢复默认设置。这样的设计好处在于简单,后期要过度到其他方案也会容易一些。具体选择什么设计,需要结合具体业务来选择。
后端服务的本地化涉及以下四部分:
locale标识属于本地化技术标准制定的。比如可使用locale = en-USHTTP 头代表请求美国地区英语语言的页面。如果后端服务的技术栈不同,还需要本地化团队总结后端服务不同技术栈的国际化流程,并在组织内部同步给其他开发团队。
在后台服务远程调用中存在调用外部服务的情况,如果调用外部服务需要先确认外部服务是否支持多语言版本,如果支持的话可以按照对接文档来集成。如果不支持需要与外部服务供应商联系确定支持计划。
由于本地化的实施涉及十几个子服务的改造,可通过 Feature Toggles 控制本地化在不同环境的开启或关闭。本地化影响的测试(单元测试、集成测试与 UI 测试)也需要通过 Feature Toggles 来控制,这样可以最小影响原服务的测试套件。
一旦所有服务都完成了本地化实施,则可以打开所有服务的本地化 Feature Toggles 将最终版本上线。
关于本地化的 Feature Toggles 有两种设计可以选择:

如上图是一个微前端架构的网站,整个网站的界面是由 A/B/C/D/E 五个服务的页面组成的。语言切换按钮在服务 A 上,当用户切换英文到中文,其他服务 B/C/D/E 需要将各自的界面切换成中文语言版本。
一种方法是在浏览器加载页面的时候将国际化(i18n)库的实例统一由服务 A 来初始化并挂载到浏览器窗口(window)对象上,服务 B/C/D/E 使用服务 A 初始化的国际化库实例对象。当语言切换时,统一由服务 A 的国际化实例对象切换所有服务的语言。
每个服务的 locale 语言文件加载可以统一由服务 A 来加载到浏览器中,这种做法的好处在于可以知道最后一个语言文件加载完毕的时机,这意味着整个页面所有服务的本地化都初始化完毕,用户可以正常切换语言了。
本地化测试验证应用程序或网站内容是否符合特定国家或地区的语言、文化和地域要求。

如上图是本地化测试需重点关注的点,更多详见这篇 Localization testing: why and how to do it 文章。
本地化中很重要的一块是选择合适的翻译管理平台(TMS),一般这类平台都有如下功能点:
主要的本地化平台:
一些对国际化以及本地化的基本流程的介绍就到此为止了。本地化是个复杂的工作,最大的难点在于对目标语言和文化的了解不足。不过当你读完这篇文章后,我希望能给你更多的自信去做本地化相关的工作。
一个成功的产品要走向全球需要经历很多环节,从软件开发的视角主要有国际化和本地化两个流程:

语言环境是在某个国家或地域内使用特定的语言或语言变体,其决定了日期、时间、数字和货币的格式和解析方式,以及各种测量单位和时区、语言、国家与地区的翻译名称。国际化使一个软件能够处理多个语言环境,本地化使一个软件支持一个特定的地区的语言环境。这意味着全球化的流程是先使软件具备国际化的能力,之后做本地化实施使其能支持特定地区特定的语言环境。
基于他们的英文单字长度过长,常被分别简称成i18n(18意味着在“internationalization”这个单字中,i和n之间有18个字母)及L10n。使用大写的L以利区分i18n中的i和易于分辨小写l与1。(Wikipedia)
我们知道国际化是为了解决与用户本地语言相关的文本显示与输入的问题,这个问题又与用户国家和语言相关,比如同样的英语在美国和英国就不同。在国际化标准还未出现之前,曾经有多种表示国家与语言的方法,这个 Making Sense of Language Tags 的 Slide 就分享了这段有趣的国际化标准问世的历史。直到 IETF BCP(Best Current Practice) 47 的出现,才统一规定了国际化中语言标识(Language Tag)的定义及匹配标准。
而由于很多软件与系统早于此标准出现,就会出现一些与此标准不统一的问题。一个突出的问题就是语言标识定义中的连接线的选择,在 Linux 系列的操作系统中用 locale 来定义语言环境,如en_US表示美国英语,而在 BCP 47 中用en-US表示美国英语。前者选择了用_而后者采用了-来连接语言和国家。这种混乱有时候会带来很多意想不到的困惑,有时候你使用的某个库支持en_US,有的库却支持en-US,这不得不让国际化实现的过程中多了一些兼容性处理的工作,甚至因为语言不统一,出现很多沟通上的问题。
国际化相关的标准如下:
一个完整的语言标识(Language Tag)组成如下:

更详细的介绍见我这个 i18N in Java 的 Slide。
不同编码有着可以表示不同字符集合的区别,比如我们无法用 ASCII 编码来表示汉字。Unicode 字符集可以用从 0 到 10FFFF (十六进制)范围的码点来显示几乎所有人类已知的字符。它的存储至少需要 21 位。文本编码系统 UTF-8 将 Unicode 码点适配到一个合理的 8 位数据流,并兼容 ASCII 数据处理系统。UTF 表示 Unicode 转换格式(Unicode Transformation Format)。
自 2009 年以来,UTF-8 一直是万维网的最主要的编码形式。截止到 2019 年 11 月, 在所有网页中,UTF-8 编码应用率高达 94.3%(其中一些仅是 ASCII 编码,因为它是 UTF-8 的子集),而在排名最高的 1000 个网页中占 96%。所以在国际化中推荐采用 UTF-8 编码。
这篇 IT产品的国际化,绝不是“支持英文”就足够 文章提到一些 GBK 编码的文本中有许多“看起来一样”的文字,其实有细微差别。但是,为了节省 Unicode 中的空间,给它们指定了同样的 Code Point。

如何区分这些同样码位(用不同字形显示一个字符,即同一字位)的同位异字?这就需要 locale 的帮助了。
计算汉字数量时,通常是按照字形来计算的,即将一个代表相同语音语义的字的简化,繁体,异体,新字形,旧字形等等分别进行计算。这种计算方式实为是在计算变体。所以,长期以来错误地把大型字典里收入的字形数看作是汉字系统的规模。(Wikipedia)
locale 是软件在运行时的语言环境, 它包括语言(Language), 地域(Territory)和字符集(Codeset)。locale 使用 language tag 标识语言国家,比如在 GNU Linux 中的定义格式为: 语言[_地域[.字符集]],如美国英文是en_US.UTF8。在 Linux 中 locale 包括以下几个部分:
如果你的 locale 是 en_US.UTF8,那么必须将其修改为 zh_CN.UTF8 才能正确显示中文。在 macOS 操作系统的 /usr/share/locale 目录中存放着全部支持的 locale:

而在 BCP 47 中,language tag 的定义为langtag = language["-" script]["-" region]*("-" variant)*("-" extension)["-" privateuse]。
同一种语言在不同国家地区可能有一些细微的差异,比如美国英语和英国英语就有一些差异。同一个国家可能也有多种语言,比如中国有简体与繁体语言。在上述 locale 的介绍我们看到了使用语言_地域或语言-地域的方式来确切的表达一个国家的语言。
对于国家和语言 ISO 制定了相应的标准代码:ISO 3166-1 与 ISO 639-1。
浏览器使用语言代码来在 Accept-Language HTTP 头里发送浏览器接受的语言名。比如:it, de-at, es, pt-br 。
GNU gettext 是 GNU 国际化与本地化(i18n)函数库,它常被用于编写多语言(multilingualization,缩写为 M17N)程序。许多编程语言如 C、C++、Python、PHP、Rust、Elixir 等都在语言内部支持了 gettext 的使用。
以下是 Java 调用 gettext 完成国际化的流程:

ResourceBundle 类的 Java 类文件。如下图是 PHP 使用 gettext 实现国际化的流程图:

Elixir 使用 gettext 实现 i18n 的目录结构:
priv/gettext
└─ en_US
| └─ LC_MESSAGES
| ├─ default.po
| └─ errors.po
└─ it
└─ LC_MESSAGES
├─ default.po
└─ errors.po
gettext 的使用流程就是一个典型的使应用支持 i18n 国际化的过程:

如上图是一个典型的本地化流程图。其中参与方有:
这块首先要考虑这些基本的前置问题:
语言_地域或语言-地域格式,如en-US代表美国英语语言。
本地化的挑战主要有不同地域的语言、文化、书写习惯及法律方面的差异带来的问题,具体有以下类别:
如果是 toC 的网站做本地化,需要考虑一些和搜索引擎优化 (SEO) 的事情,如这篇 How to approach an international strategy 提到的一些关键点:
www.mysite.com/de/ 会告诉用户页面是德语的;hreflang元标记向 Google 显示您要定位的语言。如<link rel="alternate" href="http://example.com" hreflang="en-us" />;同一内容在不同地域使用更贴合本地内容的设计会带来更好的效果,如 产品设计的国际化与本地化 这篇文章中提到 Spotify 的歌单封面在不同国家的差异呈现形式:

从架构的角度看单体应用的本地化流程比较简单。但现在很多应用都是微服务架构,多个团队协作开发的模式。如果是各自团队负责各自服务的本地化,必须有统一的本地化委员会制定本地化技术标准:
或者有专门的本地化团队实施本地化,前面这些问题将由这个团队负责解决。笔者参与的项目就属于后者,我们的团队完成整个大系统近十几套微服务子系统的本地化,这十几套系统又由几个大组多个团队负责,这类跨功能需求(CFR)在多个团队中的协作流程是个复杂的工作。
在实施本地化之前,确定相关技术或业务标准是重要的事情,一些技术或业务标准有:
en.json存放英语的静态文本,而en_US.json存放和美国英语相关的文本(如计量单位、日期、数字和货币等);语言-地域格式,如en-US代表获取美国地区英语语言的本地化版本。实际上我们团队在做本地化最耗费时间的就是本地环境的启动。因为涉及的服务众多,不同服务启动的方式又有着细微的差异,甚至指导文档也是错误的,需要不断的踩坑才能完成环境的搭建。最终我们的处理方式是联系各开发团队,每次在做某个服务的本地化前期,会找开发团队帮助我们设置本地的环境。
另外一个难点在于我们对业务的不了解。由于每个服务都有大量的组件和页面,包括后端服务不同来源的动态数据,光靠我们自己摸索很难搞清楚。最终我们在做这个服务的本地化前期,会找开发团队的业务分析师帮助我们介绍这个服务涉及的业务流程。
一些国际化站点的语言或地区切换设计成超链接,用户可以通过链接访问不同语言地区版本的站点,这类站点并不需要存储语言或地区的配置。
具备用户个人信息配置的站点一般会提供在个人信息设置中设置偏好语言和地区,这样用户在切换设备的时候可以同步上次设置的语言或地区。
如果你的站点用户切换设备并不频繁,简单的处理可以将这些配置存入浏览器存储中。当用户切换设备后,自动恢复默认设置。这样的设计好处在于简单,后期要过度到其他方案也会容易一些。具体选择什么设计,需要结合具体业务来选择。
后端服务的本地化涉及以下四部分:
locale标识属于本地化技术标准制定的。比如可使用locale = en-USHTTP 头代表请求美国地区英语语言的页面。如果后端服务的技术栈不同,还需要本地化团队总结后端服务不同技术栈的国际化流程,并在组织内部同步给其他开发团队。
在后台服务远程调用中存在调用外部服务的情况,如果调用外部服务需要先确认外部服务是否支持多语言版本,如果支持的话可以按照对接文档来集成。如果不支持需要与外部服务供应商联系确定支持计划。
由于本地化的实施涉及十几个子服务的改造,可通过 Feature Toggles 控制本地化在不同环境的开启或关闭。本地化影响的测试(单元测试、集成测试与 UI 测试)也需要通过 Feature Toggles 来控制,这样可以最小影响原服务的测试套件。
一旦所有服务都完成了本地化实施,则可以打开所有服务的本地化 Feature Toggles 将最终版本上线。
关于本地化的 Feature Toggles 有两种设计可以选择:

如上图是一个微前端架构的网站,整个网站的界面是由 A/B/C/D/E 五个服务的页面组成的。语言切换按钮在服务 A 上,当用户切换英文到中文,其他服务 B/C/D/E 需要将各自的界面切换成中文语言版本。
一种方法是在浏览器加载页面的时候将国际化(i18n)库的实例统一由服务 A 来初始化并挂载到浏览器窗口(window)对象上,服务 B/C/D/E 使用服务 A 初始化的国际化库实例对象。当语言切换时,统一由服务 A 的国际化实例对象切换所有服务的语言。
每个服务的 locale 语言文件加载可以统一由服务 A 来加载到浏览器中,这种做法的好处在于可以知道最后一个语言文件加载完毕的时机,这意味着整个页面所有服务的本地化都初始化完毕,用户可以正常切换语言了。
本地化测试验证应用程序或网站内容是否符合特定国家或地区的语言、文化和地域要求。

如上图是本地化测试需重点关注的点,更多详见这篇 Localization testing: why and how to do it 文章。
本地化中很重要的一块是选择合适的翻译管理平台(TMS),一般这类平台都有如下功能点:
主要的本地化平台:
一些对国际化以及本地化的基本流程的介绍就到此为止了。本地化是个复杂的工作,最大的难点在于对目标语言和文化的了解不足。不过当你读完这篇文章后,我希望能给你更多的自信去做本地化相关的工作。