静态站点的国际化和本地化
已发表: 2022-03-10国际化和本地化不仅仅是用多种语言编写您的内容。 您需要一种策略来确定要发送的本地化内容以及执行此操作的代码。 您不仅需要能够支持不同的语言,还需要支持使用相同语言的不同地区。 你的 UI 需要响应,不仅是屏幕大小,而是不同的语言和书写模式。 您的内容需要结构化,细化到 UI 中的缩微副本和日期格式,以适应您使用的任何语言。 使用像 Eleventy 这样的静态站点生成器来完成所有这些工作会变得更加困难,因为您可能没有数据库,但也没有服务器。 不过,这一切都可以完成,但这需要计划。
在构建 chromeOS.dev 时,我们知道我们需要将其提供给全球观众。 确保我们的代码库可以支持多种语言环境(语言、地区或两者的组合),而无需对每个语言环境进行自定义编码,同时允许使用尽可能少的系统知识来完成翻译,这对于制作这发生了。 我们的内容创建者需要能够专注于创建内容,而我们的翻译人员则需要专注于翻译内容,尽可能少地把他们的工作放到网站上并进行部署。 正确处理这些有时相互冲突的需求是实现代码库国际化和网站本地化的核心。
国际化 (i18n) 和本地化 (l10n) 是同一枚硬币的两个方面。 国际化是关于如何,在我们的情况下,软件设计,使其可以适用于多种语言和地区,而无需工程更改。 另一方面,本地化实际上是针对这些语言和地区调整软件。 国际化可以在整个网站堆栈中发生; 从 HTML、CSS 和 JS 到设计注意事项和构建系统。 本地化主要发生在内容创建(长文和微文)和管理中。
注意:对于那些好奇,I18N和L10N是称为Numer更名的缩写类型。 A11Y,对于Accordisibility,是Web开发中的另一个常见的Numer选n。
国际化 (i18n)
在确定国际化时,您通常需要考虑三个事项:如何确定用户想要什么语言和/或地区,如何确保他们获得他们喜欢的本地化内容,以及如何调整您的网站以适应那些差异。 虽然动态站点(在用户请求时呈现页面)和静态站点(在部署之前构建页面)的实现细节可能会发生变化,但核心概念应该保持不变。
确定用户的语言和地区
确定国际化时要考虑的第一件事是确定您希望用户如何访问本地化内容。 这一决定将成为您如何设置其他系统的基础,因此重要的是要提前决定并确保权衡适合您的用户。
通常,有三种高级方法可以确定向用户提供何种本地化服务:
- 来自 IP 地址的位置;
-
Accept-Language
标头或navigator.languages
; - URL 中的标识符。
在决定提供什么本地化服务时,许多系统最终会结合一个、两个或所有三个。 然而,在我们调查的过程中,我们发现了使用 IP 地址和Accept-Language
标头的问题,我们认为这些问题足以让我们不再考虑:
- 用户的首选语言通常与 IP 地址提供的物理位置无关。 例如,仅仅因为某人实际位于美国,并不意味着他们更喜欢英语内容。
- 从 IP 地址进行位置分析很困难,通常不可靠,并且可能会阻止网站被搜索引擎抓取。
-
Accept-Language
标头通常从不明确设置,仅提供有关语言的信息,而不是区域信息。 由于其局限性,这可能有助于建立对语言的初步猜测,但不一定可靠。
由于这些原因,我们决定最好不要在用户登陆我们的网站之前尝试推断语言或地区,而是在我们的 URL 中设置强有力的指标。 拥有强大的指标还允许我们假设他们仅通过访问 URL 以他们想要的语言获取站点,提供了一种直接共享本地化内容的简单方法,而无需担心重定向,并为我们提供了一种清晰的方法来让用户切换他们的首选语言。
将标识符构建到URL有三种常见模式:
- 提供不同的域(通常是不同地区和语言的TLD或子域(例如
example.com
和example.de
,en.example.org
和de.example.org
); - 具有本地化的内容子目录(例如
example.com/en
和example.com/de
); - 根据 URL 参数(例如
example.com?loc=en
和example.com?loc=de
)提供本地化内容。
虽然常用,但通常不推荐使用 URL 参数,因为用户很难识别本地化(以及许多分析和管理问题)。 我们还决定不同的域名对我们来说不是一个很好的解决方案; 我们的网站是一个渐进式 Web 应用程序,每个域(包括 TLD 和子域)都被视为不同的来源,实际上每个本地化都需要单独的 PWA。
我们决定使用子目录,这使我们能够根据需要仅本地化语言( example.com/en
)或语言和地区( example.com/en-US
和example.com/en-GB
),同时维护单个 PWA。 我们还决定,我们网站的每个本地化版本都位于一个子目录中,因此一种语言不会高于另一种语言,并且除了子目录之外,所有 URL 在基于创作语言的本地化版本中都是相同的,允许用户轻松更改无需翻译 URL 即可进行本地化。
提供本地化内容
一旦确定了确定用户语言和地区的策略,您就需要一种方法来可靠地为他们提供正确的内容。 至少,这将需要某种形式的存储信息,无论是 cookie、一些本地存储还是应用程序自定义逻辑的一部分。 能够保持用户的本地化偏好是 i18n 用户体验的重要组成部分; 如果用户确定他们想要德语内容,并且他们登陆英语内容,您应该能够识别他们的首选语言并适当地重定向它们。 这可以在服务器上完成,但我们为 chromeOS.dev 采用的解决方案是托管和服务器设置不可知:我们使用服务工作者。 用户的旅程如下:
- 用户第一次访问我们的网站。 我们的服务人员没有安装。
- 无论他们采用何种本地化语言,我们都将其设置为 IndexedDB 中的首选语言。 为此,我们认为他们通过一些手段,社交,推荐或搜索,这是基于我们没有的其他本地化上下文的方式。 如果用户登陆时没有设置本地化设置,我们会将其设置为英语,因为这是我们网站的主要语言。 我们的页脚中还有一个语言切换器,以允许用户更改他们的语言。 此时,我们的 service worker 应该已经安装好了。
- 安装了服务工作者后,我们拦截站点导航的所有URL请求。 因为我们的本地化是基于子目录的,所以我们可以很容易地确定正在请求什么本地化。 一旦确定,我们检查请求的页面是否在本地化子目录中,检查本地化子目录是否在支持的本地化列表中,并检查本地化子目录是否与存储在 IndexedDB 中的首选项匹配。 如果它不在本地化子目录中或本地化子目录与他们的偏好匹配,我们提供页面; 否则,我们会从我们的服务人员那里进行 302 重定向,以获得正确的本地化。
我们将我们的解决方案捆绑到Workbox Plugin中,服务工作者国际化重定向。 当与 Workbox 的registerRoute
方法和request.mode === 'navigate'
上的过滤请求结合使用时,该插件及其首选项子模块可以组合以设置和获取用户的语言首选项并管理重定向。
一个完整的、最小的示例如下所示:
客户代码
import { preferences } from 'service-worker-i18n-redirect/preferences'; window.addEventListener('DOMContentLoaded', async () => { const language = await preferences.get('lang'); if (language === undefined) { preferences.set('lang', lang.value); // Language determined from localization user landed on } });
服务工作者代码
import { StaleWhileRevalidate } from 'workbox-strategies'; import { CacheableResponsePlugin } from 'workbox-cacheable-response'; import { i18nHandler } from 'service-worker-i18n-redirect'; import { preferences } from 'service-worker-i18n-redirect/preferences'; import { registerRoute } from 'workbox-routing'; // Create a caching strategy const htmlCachingStrategy = new StaleWhileRevalidate({ cacheName: 'pages-cache', plugins: [ new CacheableResponsePlugin({ statuses: [200], }), ], }); // Array of supported localizations const languages = ['en', 'es', 'fr', 'de', 'ko']; // Use it for navigations registerRoute( ({ request }) => request.mode === 'navigate', i18nHandler(languages, preferences, htmlCachingStrategy), );
结合客户端和服务工作者代码,用户的首选本地化将在他们第一次访问网站时自动设置,如果他们导航到不在他们首选本地化中的 URL,他们将重定向。
调整站点用户界面
正确调整用户界面有很多内容,因此虽然这里不会涵盖所有内容,但可以而且应该以编程方式管理一些更微妙的事情。
块报价行情
一个常见的设计模式具有包裹在引号中的Blockquotes,但您是否知道用于这些引号使用的内容因本地化而变化? 代替硬编码,使用open-quote
和close-quote
来确保正确的引号用于正确的语言。

lang=“en”
的open-quote
和close-quote
显示为两个上标逗号,它们向内朝向文本,第一对倒置。 (大预览) 
lang=“fr”
的open-quote
和close-quote
显示为一对 V 形,它们的开口向内朝向文本。 (大预览)日期和数字格式
日期和数字都有一个方法, .toLocaleString
允许基于本地化(语言和/或区域)进行格式化。 支持这些的浏览器附带所有可用的本地化版本,使其在此处易于使用,但 Node.js 没有。 幸运的是,Node 的完整 icu 模块允许您使用所有可用的本地化数据。 为此,在安装模块后,使用设置为模块的路径的NODE_ICU_DATA
环境变量运行代码,例如NODE_ICU_DATA=node_modules/full-icu
。

HTML 元信息
您的 HTML 标记和标题中的三个区域应随每次本地化而更新:
- 页面的语言,
- 写作方向,
- 页面可用的替代语言。
第一个分别使用dir
和lang
属性的html
元素,例如美国英语的<html lang="en" dir-"ltr">
。 正确设置这些将确保内容朝着正确的方向流动,并且可以让浏览器了解页面的语言,从而允许翻译内容等附加功能。 您还应该包含rel="alternate"
链接,让搜索引擎知道页面已完全翻译,因此在我们的英文着陆页上包含<link href="/es" rel="alternate" hreflang="es">
将让搜索引擎知道这有一个翻译它应该在寻找。
内在设计
本地化内容可能会带来设计挑战,因为不同的翻译将在页面上占用不同数量的空间。 某些语言(例如德语)的单词较长,需要更多的水平空间或更宽泛的文本换行。 其他语言,如阿拉伯语,具有更高的字体,需要更多的垂直空间。 幸运的是,有许多 CSS 工具可以使间距和布局不仅响应视口大小,还响应内容,这意味着它们可以更好地适应多种语言。
有许多专门为处理内容而设计的 CSS 单元。 em
和rem
单位分别代表计算的字体大小和根字体大小。 为这些单位交换固定大小的px
值可以大大提高网站对其内容的响应速度。 然后是ch
单元,表示字体中0(零)字形的内联大小。 这允许您将类似width
的内容直接绑定它包含的内容。
然后,这些单元可以与现有的强大的 CSS 布局工具(特别是 flexbox 和网格)结合到适应其大小的组件中,并且布局适应其内容。 使用边界、边距和填充的逻辑属性而不是物理物理属性来增强那些布局和组件也可以自动适应书写模式。 内在网页设计的力量(由 Jen Simmons 创造,内容感知单元和逻辑属性允许设计和构建界面,以便它们可以适应任何语言,而不仅仅是任何屏幕尺寸。
本地化 (l10n)
最明显的形式本地化需要将内容从一种语言转换为另一语言。 在更微妙的形式中,翻译不仅发生在语言上,而且发生在说它的地区,例如,美国的英语与英国、南非或澳大利亚的英语。 要在这里取得成功,了解要翻译的内容以及如何构建翻译内容对成功至关重要。
内容策略
软件项目的某些部分对本地化很重要,而有些则不是。 CSS 类名、JavaScript 变量和代码库中其他结构性但不面向用户的位置可能不需要本地化。 找出需要本地化的内容以及如何构建它,归结为内容策略。
内容策略有很多定义,但在这里它指的是内容的结构、缩微文案(整个项目中使用的单词和短语,不与特定内容相关联),以及它们之间的联系。 有关内容策略的更多详细信息,我推荐 Karen McGrane 的 Content Strategy for Mobile 以及 Carrie Hane 和 Mike Atherton 的 Designing Connected Content。
对于 chromeOS.dev,我们最终编写了描述内容结构的内容模型。 内容模型不仅仅适用于长篇文章式的内容; 内容模型应该存在于用户可能特别想从您那里获得的任何实体,例如作者、文档,甚至可重用的媒体资产。 好的内容模型包括一个更大的概念片段的可单独寻址的片段或块,同时排除切向相关或可以从另一个内容模型引用的块。 例如,博客文章的内容模型可能包含标题、标签数组、对作者的引用、发布日期和文章正文,但不应包含面包屑的字符串或作者的名字和图片,应该是自己的内容模型。 内容模型不会从本地化变为本地化; 它们是网站结构。 内容模型的实例与本地化相关联,并且这些实例可以本地化。
不过,内容模型仅涵盖需要本地化的一部分。 其余的——你的“阅读更多”按钮、你的“菜单”标题、你的免责声明文本——都是微文案。 显微文案也需要结构。 虽然创建内容模型可能感觉很自然,特别是对于模板驱动的网站,但缩微模型往往不太明显,并且经常被直接在模板中写入所需内容而被意外忽略。
通过构建内容和微拷贝模型并通过内容管理系统、linting 或审查来执行它们,您能够确保本地化可以专注于本地化。
本地化值,而不是键
内容和微拷贝模型通常会生成类似于Codebase中的对象的结构; 无论是数据库条目、JSON 对象、YAML 还是 Front Matter。 不要本地化对象键! 如果您的搜索文本显微镜位于microcopy
的微型计算机对象中,请不要将其放在microcopy.search.text
的microcopie.chercher.texte
microcopie
对象中。 模块中的键应被视为本地化 - 不可知标识符,因此它们可以可靠地用于可重用模板并依赖于整个码字样。 这也意味着对象键不应显示为最终用户作为内容或显微镜。
静态站点设置
对于 chromeOS.dev,我们使用 Eleventy (11ty) 和 Nunjucks 作为我们的静态站点生成器,但是这些设置静态站点生成器的建议应该适用于大多数静态站点生成器。 如果某些内容是 11ty 特定的,则会被调用。
文件夹结构
基于文件夹结构编译的静态站点生成器特别擅长支持子目录 i18n 方法。 11ty 还支持具有全局数据的数据级联以及通过分页从数据生成页面的方法,因此结合这三个概念会产生一个基本的文件夹结构,如下所示:
. └── pages ├── _data ├── _generated └── {{locale-code}} ├── {{locale-code}}.11tydata.js ├── _data └── [...content]
在顶层,有一个目录来保存站点的页面,这里称为pages
。 嵌套在里面,有一个包含全局数据文件的_data
文件夹。 在接下来讨论助手时,这个文件夹很重要。 然后,有一个_generated
文件夹。 我们有许多页面,而不是拥有自己的内容,来自现有内容,少量的微拷贝或两者的组合。 思考主页,搜索页面或博客部分的着陆页。 因为这些页面是高度模板化的,所以我们将模板存储在_generated
文件夹中并从那里构建它们,而不是为每个页面创建单独的 HTML 或 Markdown 文件。 这些文件夹以下划线为前缀,以指示它们不会直接输出页面,而是用于在其他地方创建页面。
接下来,l10n 子目录! 每个目录都应该以 BCP47 语言标记(更常见的是语言环境代码)为其包含的本地化命名:例如, en
表示英语,或en-US
表示美国英语。 在Chromeos.dev CodeBase中,我们也经常将这些视为Locales。 这些文件夹将成为本地化子目录,将内容分段为本地化。 11ty 的数据级联允许数据可用于目录中的每个文件及其子文件,前提是该文件位于目录的根目录并与目录命名相同(称为目录数据文件)。 11ty 使用从该文件返回的对象,或返回对象的函数,并将其注入可用于模板的变量中,因此我们可以在此处访问该本地化的所有内容的数据。
为了帮助这些文件的可维护性,我们编写了一个名为l10n-data
的帮助程序,它是我们静态站点脚手架的一部分,它利用此文件夹结构来构建级联的本地化数据,从而允许数据零散地本地化。 它通过将数据存储在特定于语言环境的数据目录中来实现这一点,其中的_data
目录(加载到目录数据文件中)。 例如,如果您查看我们的英语语言环境数据目录,您会看到诸如locale.json
之类的微拷贝模型,它定义了语言代码和编写方向,然后将呈现到我们的 HTML 中, newsletter.yml
定义了我们所需的微拷贝时事通讯注册和一个microcopy.yml
文件,其中包括在整个站点中的多个位置中使用的一般微孔,其不适合更具体的文件。 在任何使用此微副本的任何地方,我们都会从通过 11ty 将数据变量注入到我们的模板中以供使用的可用数据中提取它。
微文案往往是最难管理的,而其余内容大多是直截了当的。 将内容常常将文件或HTML放入本地化的子文件夹中。 对于处理文件夹结构的静态站点生成器,内容的文件名和文件夹结构通常会 1:1 映射到该内容的最终 URL,因此位于en/web/pwas.md
的 Markdown 文件将输出到 URL en/web/pwa
。 遵循我们的“值,而不是键”本地化原则,我们决定不本地化内容文件名(以及路径),以便我们更轻松地跨语言环境跟踪同一文件的本地化状态并让用户知道它们位于不同语言环境之间的正确页面上。
I18N帮助者
除了内容和微文案之外,我们发现我们还需要编写一些帮助模块,以便更轻松地处理本地化内容。 11tty有一个名为筛选器的概念,允许在呈现之前修改内容。 我们最终构建了其中的四个来帮助 i18n 模板。
第一个是日期过滤器。 我们标准化将我们内容中的所有日期写为 YAML 日期值,因为我们主要用 YAML 编写它们,并且它们在我们的模板中作为完整的 UTC 时间戳可用。 当使用full-icu
模块和配置时,日期字符串(正在更改的内容)以及正在呈现的内容的语言环境代码,可以直接传递给Date.toLocaleString
(带有可选的格式选项)以呈现本地化日期。 Date.toLocaleDateString
如果您只需要没有传入格式选项时的日期部分,而不是完整的本地化日期和时间,则可以选择使用 Date.toLocaleDateString。
第二个过滤器是我们称之为localURL
的东西。 这需要一个本地 URL(正在更改的内容)和 URL 应该所在的语言环境,然后交换它们。 例如,它将/en/linux
更改为/es/linux
。
最后两个过滤器是关于单独从区域设置代码中检索本地化信息。 第三个利用 iso-639-10 模块将语言环境代码转换为本地语言的语言名称。 这主要用于我们的语言选择器。 第四个使用 iso-i18n-countries 模块来检索该语言的国家列表。 这主要用于构建带有国家/地区列表的表单。
除了过滤器,11ty 还有一个叫做集合的概念,它是一组内容。 默认情况下,11ty 提供了许多可用的集合,甚至可以从标签构建集合。 在一个多语言站点中,我们发现我们想要构建自定义集合。 我们卷起了许多辅助函数来构建基于本地化的集合。 这使我们能够执行诸如具有特定于位置的标签集合或站点部分集合之类的操作,而无需在我们的模板中针对我们站点上的所有内容进行过滤。
我们最后的也是最关键的助手是我们的站点全局数据。 依靠基于语言环境代码的子目录结构,此函数动态确定站点支持的本地化。 它构建了一个全局变量,包括site
属性,其中包含来自{{locale-code}}.11tydata.js
l10n
它还包含一种languages
属性,该属性将所有可用的本地列为数组。 最后,该函数输出一个 JavaScript 文件,详细说明站点支持哪些语言以及{{locale-code}}.11tydata.js
中每个条目的各个文件,每个本地化都键入,所有这些都设计为由我们的浏览器脚本导入。 大量提升此文件将我们的静态站点与我们的前端JavaScript联系起来,单一的真理来源是我们已经需要的本地化信息。 它还允许我们通过循环site.l10n
以编程方式基于我们的本地化生成页面。 这与我们的本地化特定集合相结合,让我们使用 11ty 的分页来创建本地化的主页和新闻登录页面,而无需为每个页面维护单独的 HTML 页面。
结论
获得国际化和本地化权力可能很困难; 了解如何不同的策略和影响复杂性对于使其更容易至关重要。 选择一个自然适合静态站点的I18N策略,然后将工具从正在生成的内容中自动构建以自动化I18N和I10N的部分。 构建强大的内容和缩微模型。 利用服务工作者进行与服务器无关的本地化。 将它与一个响应于屏幕尺寸的设计绑定,但内容的内容。 最后,您将拥有一个网站,您的所有语言环境都会喜欢作者和翻译人员可以维护,就像它是一个简单的单个区域设置网站一样。