Cloudflare 的仪表板现在支持四种新的语言(和多种区域设置):西班牙语(以及特定国家/地区的区域设置:智利、厄瓜多尔、墨西哥、秘鲁和西班牙)、巴西葡萄牙语、韩语和繁体中文。我们的客户来自世界各地,有着多样的背景,因此在帮助所有人建设更美好的互联网时,必须以客户的母语来为他们提供产品和服务。

自去年以来,Cloudflare 一直在奋力开展仪表板国际化工作。2019 年底,我们推出了美国英语外的第一种语言:德语。2020 年 3 月,我们又发布了三种语言:法语、日语和简体中文。如果您想要以当中任何一种语言使用仪表板,可以在 Cloudflare 仪表板的右上方更改语言首选项。首选项选择后会保存下来,在所有会话中使用。

在这篇博文中,我想帮助那些不熟悉国际化和本地化的人更好地了解它的运作原理。我还想讲一讲我们如何以一个可重复的标准过程进行应用程序国际化和本地化,并且分享一些可能会对您有所裨益的贴士。

踏上征途

国际化的第一步是外部化应用程序中的所有字符串。具体来说,需要将所有可被用户读取的文本从应用程序代码中提取出来,放入另外的独立文件中。需要这样做的原因有几个:

  • 它使翻译团队能够翻译这些字符串,而无需查看或更改任何应用程序代码。
  • 大多数译员通常使用翻译管理应用程序,这些应用程序可以自动化工作流的各个方面,并为他们提供有用的实用程序(如翻译记忆库、更改跟踪,以及许多有用的解析和格式工具)。这些应用程序需要标准化的文本格式(例如 json、xml、md 或 csv 文件)。

从工程角度来看,将应用程序代码与翻译分开,可以在不重新编译和/或重新部署代码的情况下对字符串进行更改。在基于 React 的应用程序中,将大多数字符串外部化归结为更改代码块,如下所示:

<Button>Cancel</Button>
<Button>Next</Button>

变成:

<Button><Trans id="signup.cancel" /></Button>
<Button><Trans id="signup.next" /></Button>
 
// And in a separate catalog.json file for en_US:
{
 "signup.cancel": "Cancel",
 "signup.next": "Next",
 // ...many more keys
}

上方所示的 <Trans> 组件是我们应用程序中的基本 i18n 构建块。在此方案中,翻译后的字符串保存在以翻译 ID 为键的大型字典中。我们将这些字典称为“翻译目录”,而且我们支持的每种语言都有一套翻译目录。

在运行时,<Trans> 组件在正确的目录中为提供的键查找对应翻译,然后将翻译插入页面(通过 DOM)。应用程序的所有静态文本都可以通过像这样的简单转换来外部化。

然而,当动态数据需要与静态文本混合在一起时,解决方案就会变得有些复杂。思考下面这个例子,它看似简单明了,却藏有 i18n 地雷:

<span>You've selected { totalSelected } Page Rules.</span>

在外部化这个句子时,我们轻易会把它切割为几个部分,比如:

<span>
 <Trans id="selected.prefix" /> {totalSelected } <Trans id="pageRules" />
</span>
 
// English catalog.json
{
 "selected.prefix": "You've selected",
 "pageRules": "Page Rules",
 // ...
}
 
// Japanese catalog.json
{
 "selected.prefix": "選択しました",
 "pageRules": "ページ ルール",
 // ...
}
 
// German catalog.json
{
 "selected.prefix": "Sie haben ausgewählt",
 "pageRules": "Page Rules",
 // ...
}
 
// Portuguese (Brazil) catalog.json
{
 "selected.prefix": "Você selecionou",
 "pageRules": "Page Rules",
 // ...
}

这便完成了工作,甚至看起来似乎颇为完美。毕竟,selected.prefix 和 pageRules.suffix 字符串似乎都注定要被重复使用。遗憾的是,在将字符串外部化以进行国际化时,最大的陷阱就在于将句子切碎然后串联翻译后的碎片。

问题是翻译之后,组成句子的不同词语可能会根据上下文(单复数形式、词语性别、主语/动词一致等)以不同的方式改变形态。这在不同语言之间有很大差异,词序也是如此。例如,英语中的“We like them”采用主语-谓词-宾语顺序,而其他语言则可能使用主语-宾语-谓词(We them like)、谓词-主语-宾语(Like we them),甚至是其他顺序。由于语言之间的细微差别,将翻译的短语串联成句子几乎总会导致本地化错误。

上方代码示例包含“You’ve selected”和“Page Rules”的翻译,这是我们将它们作为单独的字符串提供给翻译团队后实际获得的翻译。以下是这个句子在不同语言中呈现的模样:

语言 翻译
日语 選択しました { totalSelected } ページ ルール。
德语 Sie haben ausgewählt { totalSelected } Page Rules
葡萄牙语(巴西) Você selecionou { totalSelected } Page Rules.

为了进行比较,我们还使用变量占位符将句子作为单个字符串提供给翻译团队,结果如下:

语言 翻译
日语 %{ totalSelected } 件のページ ルールを選択しました。
德语 Sie haben %{ totalSelected } Page Rules ausgewählt.
葡萄牙语(巴西) Você selecionou %{ totalSelected } Page Rules.

如您所见,日语和德语的翻译存在差别。我们发现了一个本地化错误。

因此,为了保证翻译能够忠实传达文本的真实含义,将每个句子作为一个完整的字符串来外部化是很重要的。我们的 <Trans>组件能够将值轻松注入模板字符串中,使我们能够确切做到这一点:

<span>
  <Trans id="pageRules.selectedForDeletion" values={{ count: totalSelected }} />
</span>

// English catalog.json
{
  "pageRules.selected": "You've selected %{ count } Page Rules.",
  // ...
}

// Japanese catalog.json
{
  "pageRules.selected": "%{ count } 件のページ ルールを選択しました。",
  // ...
}

// German catalog.json
{
  "pageRules.selected": "Sie haben %{ count } Page Rules ausgewählt.",
  // ...
}

// Portuguese(Brazil) catalog.json
{
  "pageRules.selected": "Você selecionou %{ count } Page Rules.",
  // ...
}

这使译者可以掌握句子的完整上下文,确保所有词语都可以用正确的变形来翻译。

您可能注意到了另一个潜在问题。在本例中,totalSelected仅为 1 时会怎样?如果使用上面的代码,用户将看到“You've selected 1 Page Rules for deletion”。我们需要根据动态数据的值有条件地呈现句子的复数形式。事实证明,这样的用例相当普遍,而我们的 <Trans>组件通过 smart_count功能来自动解决问题:

<span>
  <Trans id="pageRules.selectedForDeletion" values={{ smart_count: totalSelected }} />
</span>

// English catalog.json
{
  "pageRules.selected": "You've selected %{ smart_count } Page Rule. |||| You've selected %{ smart_count } Page Rules.",
}

// Japanese catalog.json
{
  "pageRules.selected": "%{ smart_count } 件のページ ルールを選択しました。 |||| %{ smart_count } 件のページ ルールを選択しました。",
}

// German catalog.json
{
  "pageRules.selected": "Sie haben %{ smart_count } Page Rule ausgewählt. |||| Sie haben %{ smart_count } Page Rules ausgewählt.",
}

// Portuguese (Brazil) catalog.json
{
  "pageRules.selected": "Você selecionou %{ smart_count } Page Rule. |||| Você selecionou %{ smart_count } Page Rules.",
}

在这里,单数和复数版本由 |||| 分隔。<Trans> 会根据 totalSelected 变量传递的值自动选择要使用的正确翻译。

不过,在标记与我们要作为单个字符串外部化的文本块混合在一起时,还会冒出另一个绊脚石。例如,如果您需要句子中的某些短语成为指向另一页面的链接,这时该怎么办?

<VerificationReminder>
  Don't forget to <Link>verify your email address.</Link>
</VerificationReminder>

为解决这种用例,<Trans>组件允许将任意元素注入到翻译字符串里的占位符中,如下所示:

<VerificationReminder>
  <Trans id="notification.email_verification" Components={[Link]} componentProps={[{ to: '/profile' }]} />
</VerificationReminder>

// catalog.json
{
  "notification.email_verification": "Don't forget to <0>verify your email address.</0>",
  // ...
}

在本例中,<Trans>组件会将占位符元素(<0>、<1> 等)替换成位于 Components数组中该索引处的组件类型实例。也会将 componentProps中指定的任何数据传递到该实例。上例在 React 中可以归结为以下代码:

// en-US
<VerificationReminder>
  Don't forget to <Link to="/profile">verify your email address.</Link>
</VerificationReminder>

// es-ES
<VerificationReminder>
  No olvide <Link to="/profile">verificar la dirección de correo electrónico.</Link>
</VerificationReminder>

安全第三!

上述功能足以让我们外部化字符串。但是,它有时确实会产生笨拙的重复代码,容易变得杂乱无序。两个缺陷很快现形。

首先,细小的硬编码字符串现在更容易蹑影藏形。而且,只有翻译完页面的其余部分后开发人员才清楚看到它们,通常需要经过几天或几周的反馈循环才能发现它们。若要让这些问题显现出来,常见解决方案是在开发过程中向应用程序引入伪本地化模式,该模式通过用相似的 unicode 字符替换每个字符来转换所有正确国际化的字符串。

例如,You've selected 3 Page Rules. 可能会转换成 Ýôú'Ʋè ƨèℓèçƭèδ 3 Þáϱè Rúℓèƨ。

伪本地化模式还一个便利功能。您可以将所有字符串缩进或拉长一个固定的量,从而为内容宽度差异做好准备。以下是长度拉长50% 后的同一伪本地化句子:Ýôú'Ʋè ƨèℓèçƭèδ 3 Þáϱè Rúℓèƨ. ℓôřè₥ ïƥƨú₥ δô. 这可帮助工程师和设计师找出可能存在内容长度问题的地方。我们在推出对德语的支持时首先认识到了这个问题,因为德语中的单词往往比英语长一些。

这意味着页面元素中的文本会在许多地方溢出,例如在下方的“添加”按钮中:

对于这些类型的问题,没有太多简单办法能做到不牺牲用户体验。

为获得最佳结果,需要将可变的内容宽度融入设计本身中。修复这些错误通常意味着将其发送回上游以请求新的设计,这样的过程往往非常耗时。如果您总体上对内容设计没有太多考虑,那么国际化工作可能是个不错的开端。围绕用于应用中各种元素的正文制定标准和一致性,不仅可以减少需要翻译的词语数量,而且还能免除使用新颖短语时考虑内容长度陷阱的必要。

我们遇到的另一个陷阱是翻译 ID 极容易受到拼写错误的影响,特别是冗长而重复的 ID。

来个突击测验,以下哪个翻译键会破坏我们应用?是 traffic.load_balancing.analytics.filters.origin_health_title 还是 traffic.load_balancing.analytics.filters.origin_heath_title?

由于隐匿在其他数百行更改中,很难在代码审查中发现这些更改。大多数应用都有退路,因此缺少翻译不会导致页面破坏错误。结果,如果这样的错误隐藏得足够好(例如,帮助文本弹窗),则可能完全不会引起注意。

幸运的是,随着我们在 TypeScript 中使用代码库的比例不断增加,我们能够利用类型检查器在开发人员编写代码时提供反馈。下例中的代码编辑器正在帮助我们,通过显示红色下划线来指示 id 属性无效(由于缺少“l”):

这不仅使问题更加显眼,而且还意味着违规会导致构建失败,从而防止不良代码进入代码库。

扩展区域设置文件

起初,您支持的每个区域设置可能先使用一个翻译文件。另外,您用于键的命名方案也可能在某种程度上保持简单。随着应用的扩展,翻译文件将变得过于巨大,需要分解成单独的文件。文件太大会让翻译管理应用程序不堪重负,或者如果不加以检查,会使代码编辑器无力承受。如果组合到一个文件中,我们的所有翻译字符串(不包括键)有将近 50,000 个单词。大小与《银河系漫游指南》或《第五屠宰场》的正文大致相当。

我们将翻译分为数个“目录”文件,大致对应于功能垂直面(例如 Firewall 或Cloudflare Workers)。这对于我们的开发人员而言效果不错,因为它提供了可预测的字符串查找位置,而且也使翻译目录的行数保持在可管理的长度内。对于外部翻译团队同样有效,因为单个功能垂直面是适合译者(或小团队)工作的单元。

除了按功能分类的目录外,我们还有一个公共目录文件来保存在整个应用程序中重复使用的字符串。这样,我们不仅可以使 ID 保持简短(common.delete与 some_page.some_tab.some_feature.thing.delete相比),而且降低了重复工作的可能性,因为开发人员在添加新字符串之前会习惯性地检查公共目录。

到目前为止,我们已经详细讨论了 <Trans> 组件及其功能。现在,我们来谈谈它的构建方式。

或许不足为奇,我们不想徒劳无功,从头开始提供基本的 i18n 库。由于先前为使用 Backbone 编写的应用程序的旧版部分进行了国际化工作,我们已经在使用 Airbnb 的 Polyglot 库,它是一个“用 JavaScript 编写的微小 I18n 帮助程序库”,除了其他优点外,还“基于 Airbnb 在其 Backbone.js 和 Node 应用中添加 I18n 功能的经验,提供一种简单的插值和复数解决方案”。

我们看了一些专门为国际化 React 应用程序而开发的最热门库,但最终决定继续使用 Polyglot。我们创建了 <Trans>组件,以填补与 React 之间的空白。我们选择此方向的原因有几个:

  • 不想为了迁移到新的i18n 支持库而重新国际化应用程序中的旧代码。
  • 也不想针对新、旧代码支持2 种不同的 i18n 方案,从而增加总体开销。
  • 通过自行编写 Trans 组件,我们可以灵活地编写想要的接口。几乎所有地方都会用到 Trans,所以我们希望确保对开发人员而言,它尽可能符合人体工程学。

如果您是在新的基于 React 的 Web 应用中开始接触 i18n,那么 react-intl 和i18n-next 这 2 个流行的库提供了与上述 <Trans> 类似的组件。

如前文所述, <Trans> 组件的最大痛点在于字符串必须保存到与源代码独立的文件中。在编写新代码或修改现有功能时,需要在多个文件之间切换,这非常恼人。如果翻译文件保存在目录结构中偏远的位置(通常需要如此),这就更加令人厌烦了。

一些新的 i18n 库(如 jslingui)采用基于提取的方法来处理翻译目录,以解决这个问题。在以下方案中,我们仍然使用<Trans>组件,但是将字符串保留在组件本身中,而不是在单独的目录中:

<span>
  <Trans>Hmm... We couldn't find any matching websites.</Trans>
</span>

然后,您在构建时运行的工具会负责查找所有这些字符串,并将它们提取到目录中。例如,以上代码将生成以下目录:

// locales/en_US.json
{
  "Hmm... We couldn't find any matching websites.": "Hmm... We couldn't find any matching websites.",
}

// locales/de_DE.json
{
  "Hmm... We couldn't find any matching websites.": "Hmm... Wir konnten keine übereinstimmenden Websites finden."
}

这种方法有个显著的优点,我们不再需要单独的文件!另一个优点是也不再需要类型检查 ID,因为不再可能出现拼写错误。

不过,缺点也有几个,至少我们的用例是如此。

首先,翻译人员往往会欣赏翻译键提供的上下文。它有助于组织整理,也能提供有关字符串用途的一些线索。

而且,尽管我们无需再担心翻译 ID 中的拼写错误,但我们同样对正文有些许不匹配敏感(例如,“Verify your email”与“Verify your e-mail”)。这貌似更加糟糕,因为出现这样的情况时,它会带来几乎难以检测的重复工作。我们还必须为此付费。

无论您使用哪种技术堆栈,总会有一些 i18n 库可以帮到您。选择哪一个在很大程度上取决于应用程序的技术限制,以及团队目标和文化方面的背景。

数字、日期和时间

在上文中,我们在谈论注入数据转换后的字符串时掩盖了一个主要问题:注入的数据可能还需要进行格式化,以符合用户的当地习俗。日期、时间、数字、货币和其他一些类型的数据便是如此。

我们继续使用前面的例子:

<span>You've selected { totalSelected } Page Rules.</span>

如果没有正确格式化,小数字应该会正常,但一旦数字达到以千为单位时,就会出现本地化问题,因为数字会使用符号来分组和分隔,而符号视文化不同而异。以下是几种不同的区域设置中表示三十万点零三的格式:

语言(国家/地区) 代码 格式化数据
德语(德国) de-DE 300.000,03
英语(美国) en-US 300,000.03
英语(英国) en-GB 300,000.03
西班牙语(西班牙) es-ES 300.000,03
西班牙语(智利) es-CL 300.000,03
法语(法国) fr-FR 300 000,03
印地语(印度) hi-IN 3,00,000.03
印尼文(印度尼西亚) in-ID 300.000,03
日语(日本) ja-JP 300,000.03
韩语(韩国) ko-KR 300,000.03
葡萄牙语(巴西) pt-BR 300.000,03
葡萄牙语(葡萄牙) pt-PT 300 000,03
俄语(俄罗斯) ru-RU 300 000,03

日期的格式化方式在不同国家/地区有很大差异。如果您主要是针对美国受众开发 UI,那么显示日期的方式可能会让世界上几乎所有其他地方的用户感到奇怪,而且可能不直观。除其他方面外,日期格式在分隔符选择、是否为单个数字填充零,以及日、月、年部分排序的方式方面可能会有所不同。如下是今年 3 月 4 日这个日期在几种不同区域设置中格式化后的形式:

语言(国家/地区) 代码 格式化数据
德语(德国) de-DE 4.3.2020
英语(美国) en-US 3/4/2020
英语(英国) en-GB 04/03/2020
西班牙语(西班牙) es-ES 4/3/2020
西班牙语(智利) es-CL 04-03-2020
法语(法国) fr-FR 04/03/2020
印地语(印度) hi-IN 4/3/2020
印尼文(印度尼西亚) in-ID 4/3/2020
日语(日本) ja-JP 2020/3/4
韩语(韩国) ko-KR 2020. 3. 4.
葡萄牙语(巴西) pt-BR 04/03/2020
葡萄牙语(葡萄牙) pt-PT 04/03/2020
俄语(俄罗斯) ru-RU 04.03.2020

时间格式也有很大差异。如下是在几种不同区域设置中格式化时间的方式:

语言(国家/地区) 代码 格式化数据
德语(德国) de-DE 14:02:37
英语(美国) en-US 2:02:37 PM
英语(英国) en-GB 14:02:37
西班牙语(西班牙) es-ES 14:02:37
西班牙语(智利) es-CL 14:02:37
法语(法国) fr-FR 14:02:37
印地语(印度) hi-IN 2:02:37 pm
印尼文(印度尼西亚) in-ID 14.02.37
日语(日本) ja-JP 14:02:37
韩语(韩国) ko-KR 오후 2:02:37
葡萄牙语(巴西) pt-BR 14:02:37
葡萄牙语(葡萄牙) pt-PT 14:02:37
俄语(俄罗斯) ru-RU 14:02:37

用于处理数字、日期和时间的库

确保所有受支持的区域设置中所有这些类型的数据都使用正确的格式不是一件容易的事。幸运的是,有许多发展成熟并经过考验的库可以为您提供帮助。

在项目启动时,我们广泛使用 Moment.js 库来设定日期和时间格式。这个实用库将格式日期的详细信息抽象化为不同的长度(“Jul 9th 20”、“July 9th 2020”和“Thursday”),显示相对的日期(“2 days ago”),以及其他内容。我们使用的日期几乎都通过 Moment.js 进行格式化来提高可读性,并且 Moment.js 已经对大量区域设置有相应的 i18n 支持,我们只需拨动几个开关就能正确格式化日期,投入的工作量极少。

有一些针对 Moment.js 的强烈批评(主要是它很臃肿),但与重做每个日期和时间所花费的成本相比,改换成占用空间较小的替代方案所能实现的好处并不划算。

数字是一个截然不同的故事。正如您可能想象的那样,整个仪表板中显示成千上万未经格式化的原始数字。寻找它们是一件费力的事,常常是手动过程。

为了真正对数字进行格式化,我们使用了 Intl API(根据 ECMAScript 标准定义的国际化库):

var number = 300000.03;
var formatted = number.toLocaleString('hi-IN'); // 3,00,000.03
// This probably works in the browser you're using right now!

幸运的是,浏览器对 Intl 支持近年来有了长足进步,所有现代浏览器都提供全面支持。

诸如 V8 之类的一些现代 JavaScript 引擎甚至已经告别这些库的自托管 JavaScript 实现,转而使用基于 C++ 的内置函数,大大加快了速度

不过,对旧版浏览器的支持可能有所欠缺。这里有一个简单的演示站点源代码),它使用 Cloudflare Workers 构建,可以显示日期、时间和数字在若干种区域设置中的呈现。

一些旧浏览器和操作系统组合产生的结果或许不尽人意。例如,如下是与上方相同的日期和时间在 Windows 8 加 IE 10 组合中的呈现:

如果您需要支持较旧的浏览器,可以使用某种插件来解决问题。

翻译

当所有字符串完成外部化,所有注入的数据也仔细格式化为特定于区域设置的标准后,大部分工程工作便宣告完成。到这一刻,我们可以声称应用程序已经完成了国际化,因为它经过修改了,已经便于本地化了。

接下来是本地化的过程,根据用户的语言和文化规范真正创建不同的内容。

这可不是小事。正如前文所说,我们应用程序中的字符串加在一起就是一部小说了。创建译文需要大量协调配合和专业知识,这样才能如实捕捉其中的信息,并以用户熟悉的方式传达给用户。

处理翻译工作的方式有很多:利用会说多种语言的职员,将工作外包给翻译人员或翻译机构,甚至全力以赴并组建内部翻译团队。无论是哪种情况,都需要一个顺畅的流程来传递工作流信号,并在翻译和开发团队之间移动资产。

运作良好的 i18n 计划将为开发人员提供流程上的黑盒界面 — 他们把新字符串放入翻译目录文件中并提交更改,然后无需再做投入,他们写的功能代码就可在几天之后以所有支持的区域设置进入生产环境。类似地,在运作良好的流程中,翻译人员也会幸福快乐,不用知晓开发流程和应用架构方面的细枝末节。他们收到的文件可以轻松加载到所用的工具中,而且清楚指明需要完成哪些翻译工作。

那么,在现实中如何运作呢?

我们有一套可由本地化团队按需运行的自动化脚本,它可以针对所有支持的语言打包我们本地化目录的快照。此过程中会发生一些事情:

  • 从使用TypeScript 编写的目录文件生成 JSON 文件
  • 如果英语中添加了任何新的目录文件,则为所有其他受支持的语言创建占位符副本。
  • 有新字符串添加到我们的基本目录时,为所有语言添加占位符字符串

到这一步后,通过 UI 或对 API 的自动调用将翻译目录上传到翻译管理系统。在交给翻译人员之前,通过将每个新字符串与翻译记忆库(以前翻译的字符串和子字符串的缓存)进行比较,对文件进行预处理。如果找到匹配项,则使用现有翻译。这不仅能通过不重复翻译字符串来节省成本,而且能确保尽可能使用先前审阅和批准的译文来提高质量。

假设您的区域设置文件最终看起来像这样:

{
 "verify.button": "Verify Email",
 "other.verify.button": "Verify Email",
 "verify.proceed.link": "Verify Email to proceed",
 // ...
}

在这里,我们有逐字照搬的字符串,也有拷贝的子字符串。翻译服务是按单词数收费的。谁也不想支付两次费用,并且面临出现一致性问题的风险。为此,拥有一个维护良好的翻译记忆库能确保这些字符串在预翻译步骤中得到妥善处理,甚至是在翻译人员看到文件之前。

翻译工作标记为准备就绪后,翻译团队可能需要数小时到数周不等的时间来完成翻译工作,具体取决于诸多因素,如工作量大小、翻译人员闲忙情况,以及相关的合同条款。此阶段的关注点或可写成另一篇类似长度的博文:寻找合适的翻译团队、控制成本、保证质量和一致性,确保正确传达公司品牌形象,等等。本文的重点主要在于技术,我们就先忽略这些细节了。但请不要误解,这一部分出错会让您前功尽弃,即使您的技术目标已经达成。

翻译团队发出新文件准备就绪的信号后,相关的资产会从服务器中提取出来并解压到应用程序代码中的正确位置。然后,我们运行一套自动化检查,以确保所有文件均有效且没有任何格式问题。

这个阶段要完成一个可选(但强烈建议的)步骤 — 上下文审阅。一组翻译审阅者会在上下文中查看翻译后的输出,确保一切在最终状态下看起来尽善尽美。开展这项工作时,拥有既精通产品又能熟练使用目标语言的支持人员会特别有帮助。向公司中所有花费时间和精力来做这件事的团队成员大声道谢。为了使外部承包商可以承担此工作,我们准备了应用程序的特殊预览版本,让他们能够在启用开发模式区域设置的情况下进行测试。

到此,您便拥有了向全球用户交付应用程序本地化版本所需的一切。

持续本地化

这里应该可以停止了,但到此刻为止,我们讨论的都只是一次所需的工作。众所周知,代码会发生变化。随着新功能的发布和调整,不时会有新字符串逐渐添加、修改和删除。

翻译在很大程度上需要人类介入,常常涉及来自全球不同角落的人的投入,因此可行的更新周期会有一个下限。由于我们的发布节奏(每天)通常快于此更新频率(2-5 天),这意味着对功能进行更改的开发人员必须做出选择:减慢速度以匹配这种节奏,或者比本地化进度稍微提前一些交付而不完全覆盖。

为了确保翻译之前交付的功能不会导致应用程序中断错误,我们会在配置的语言中没有某个字符串时使它回退到基本区域设置(en_US)。

一些应用程序的回退行为略有不同:显示原始翻译键(也许您在使用的应用中看到过 some.funny.dot.delimited.string)。这里要在速度和正确性之间权衡,而我们选择针对速度和最小开销进行优化。在某些应用中,正确性的重要程度足以减慢 i18n 的节奏,而我们并未发生这种情况。

尽善尽美

在我们新近本地化的应用程序中,还可以做些事情来优化用户体验。

首先,确保没有任何性能衰退。如果应用程序让用户在呈现页面之前获取所有翻译字符串,则肯定会发生这种衰退。因此,为了使所有内容顺畅运行,仅当应用程序需要在页面上呈现某些内容时,才异步获取相应翻译目录。如今这很容易实现,借助支持动态导入语句的模块捆绑器中提供的代码拆分功能便可,如 ParcelWebpack

我们还希望消除用户在访问不同 Cloudflare 资产时必须不断选择所需语言所带来的不畅。为此,我们确保用户在我们营销网站支持网站上做出的任何语言偏好选择会在浏览到仪表板或离开之时依然保留(为了充分说明这一点,所有链接都是法语链接)。

下一步是什么?

这是一段精彩纷呈的旅程,其过程带给我们很多收获。很难(也许不可能)说一个 i18n 项目真正宣告结束。扩展到新语言时会冒出狡猾的错误,暴露新的挑战。预算压力会迫使您寻找削减成本和提升效率的方法。此外,您还会发现可以为用户进一步增强本地化体验的方法。

我们有许多方面需要改进,但是这里有一些要点:

  • 整理。字符串比较是语言敏感的;因此,如果编写的代码按字典顺序对应用程序中的列表和数据表排序,那么对某些用户来说可能是不适合的。这一点在使用语标文字系统的语言(如中文或日语)中体现的尤为明显,它们与使用字母的语言(如英语或西班牙语)截然不同。
  • 支持从右到左的语言,如阿拉伯语和希伯来语。
  • 本地化 API 响应比本地化用户界面中的静态文字更加困难,因为这需要不同团队之间的协调配合。在微服务时代,要找到一种在为各种服务提供支持的无数技术堆栈中都得心应手的解决方案,是非常具有挑战性的。
  • 本地化地图。我们将努力确保基于地图的可视化呈现中的所有内容都得到翻译。
  • 机器翻译在最近几年有了很大发展,但还不足以在无人监督前提下搅动我们的翻译。不过,我们希望进行更多的尝试,将机器翻译作为第一阶段,然后由翻译审阅人员进行编辑以确保正确性和基调。

希望您喜欢这篇关于 Cloudflare 如何国际化和本地化我们仪表板的概述。欢迎您访问我们的事业发展页面,了解有关全球全职职位和实习岗位的更多信息。