靜態站點的國際化和本地化

已發表: 2022-03-10
快速總結↬國際化和本地化不僅僅是用多種語言編寫您的內容。 您需要一種策略來確定要發送的本地化內容以及執行此操作的代碼。 您不僅需要能夠支持不同的語言,還需要支持使用相同語言的不同地區。 你的 UI 需要響應,不僅是屏幕大小,而是不同的語言和書寫模式。 您的內容需要結構化,細化到 UI 中的縮微副本和日期格式,以適應您使用的任何語言。 使用像 Eleventy 這樣的靜態站點生成器來完成所有這些工作會變得更加困難,因為您可能沒有數據庫,但也沒有服務器。 不過,這一切都可以完成,但這需要計劃。

國際化和本地化不僅僅是用多種語言編寫您的內容。 您需要一種策略來確定要發送的本地化內容以及執行此操作的代碼。 您不僅需要能夠支持不同的語言,還需要支持使用相同語言的不同地區。 你的 UI 需要響應,不僅是屏幕大小,而是不同的語言和書寫模式。 您的內容需要結構化,細化到 UI 中的縮微副本和日期格式,以適應您使用的任何語言。 使用像 Eleventy 這樣的靜態站點生成器來完成所有這些工作會變得更加困難,因為您可能沒有數據庫,但也沒有服務器。 不過,這一切都可以完成,但這需要計劃。

在構建 chromeOS.dev 時,我們知道我們需要將其提供給全球觀眾。 確保我們的代碼庫可以支持多種語言環境(語言、地區或兩者的組合),而無需對每個語言環境進行自定義編碼,同時允許使用盡可能少的系統知識來完成翻譯,這對於製作這發生了。 我們的內容創建者需要能夠專注於創建內容,而我們的翻譯人員則需要專注於翻譯內容,盡可能少地把他們的工作放到網站上並進行部署。 正確處理這些有時相互衝突的需求是實現代碼庫國際化和網站本地化的核心。

國際化 (i18n) 和本地化 (l10n) 是同一枚硬幣的兩個方面。 在我們的案例中,國際化是關於如何設計軟件,以便它可以適應多種語言和地區,而無需進行工程更改。 另一方面,本地化實際上是針對這些語言和地區調整軟件。 國際化可以在整個網站堆棧中發生; 從 HTML、CSS 和 JS 到設計注意事項和構建系統。 本地化主要發生在內容創建(長文和微文)和管理中。

注意對於那些好奇的人,i18n 和 l10n 是稱為數字名稱的縮寫類型。 A11y 表示可訪問性,是 Web 開發中的另一個常見數字。

跳躍後更多! 繼續往下看↓

國際化 (i18n)

在確定國際化時,您通常需要考慮三個事項:如何確定用戶想要的語言和/或地區,如何確保他們獲得他們喜歡的本地化內容,以及如何調整您的網站以適應那些差異。 雖然動態站點(在用戶請求時呈現頁面)和靜態站點(在部署之前構建頁面)的實現細節可能會發生變化,但核心概念應該保持不變。

確定用戶的語言和地區

確定國際化時要考慮的第一件事是確定您希望用戶如何訪問本地化內容。 這個決定將成為您如何設置其他系統的基礎,因此儘早做出決定並確保權衡對您的用戶有效是很重要的。

通常,有三種高級方法可以確定向用戶提供何種本地化服務:

  1. 來自 IP 地址的位置;
  2. Accept-Language標頭或navigator.languages
  3. URL 中的標識符。

在決定提供什麼本地化服務時,許多系統最終會結合一個、兩個或所有三個。 然而,在我們調查的過程中,我們發現了使用 IP 地址和Accept-Language標頭的問題,我們認為這些問題足以讓我們不再考慮:

  • 用戶的首選語言通常與 IP 地址提供的物理位置無關。 例如,僅僅因為某人實際位於美國,並不意味著他們更喜歡英語內容。
  • 從 IP 地址進行位置分析很困難,通常不可靠,並且可能會阻止網站被搜索引擎抓取。
  • Accept-Language標頭通常從不明確設置,僅提供有關語言的信息,而不是區域信息。 由於其局限性,這可能有助於建立對語言的初步猜測,但不一定可靠。

由於這些原因,我們決定最好不要在用戶登陸我們的網站之前嘗試推斷語言或地區,而是在我們的 URL 中設置強有力的指標。 擁有強大的指標還可以讓我們假設他們僅通過訪問 URL 以他們想要的語言獲取網站,提供了一種直接共享本地化內容的簡單方法,而無需擔心重定向,並為我們提供了一種干淨的方式讓用戶切換他們的首選語言。

將標識符構建到 URL 中有三種常見模式:

  1. 提供不同的域(通常是針對不同地區和語言的 TLD 或子域(例如example.comexample.deen.example.orgde.example.org );
  2. 具有本地化的內容子目錄(例如example.com/enexample.com/de );
  3. 根據 URL 參數(例如example.com?loc=enexample.com?loc=de )提供本地化內容。

雖然常用,但通常不推薦使用 URL 參數,因為用戶很難識別本地化(以及許多分析和管理問題)。 我們還認為不同的域對我們來說不是一個好的解決方案; 我們的網站是一個漸進式 Web 應用程序,每個域(包括 TLD 和子域)都被視為不同的來源,實際上每個本地化都需要單獨的 PWA。

我們決定使用子目錄,這使我們能夠根據需要僅本地化語言( example.com/en )或語言和地區( example.com/en-USexample.com/en-GB ),同時維護單個 PWA。 我們還決定,我們網站的每個本地化版本都位於一個子目錄中,因此一種語言不會高於另一種語言,並且除了子目錄之外,所有 URL 在基於創作語言的本地化版本中都是相同的,允許用戶輕鬆更改無需翻譯 URL 即可進行本地化。

提供本地化內容

一旦確定了確定用戶語言和地區的策略,您就需要一種方法來可靠地為他們提供正確的內容。 至少,這將需要某種形式的存儲信息,無論是 cookie、一些本地存儲還是應用程序自定義邏輯的一部分。 能夠保持用戶的本地化偏好是 i18n 用戶體驗的重要組成部分; 如果用戶確定他們想要德語內容,並且他們登陸英語內容,您應該能夠識別他們的首選語言並適當地重定向它們。 這可以在服務器上完成,但我們為 chromeOS.dev 採用的解決方案是託管和服務器設置不可知:我們使用服務工作者。 用戶的旅程如下:

  • 用戶第一次訪問我們的網站。 我們的服務人員沒有安裝。
  • 無論他們採用何種本地化語言,我們都將其設置為 IndexedDB 中的首選語言。 為此,我們假設他們是通過某種方式登陸那裡的,無論是社交、推薦還是搜索,這些方式已經根據我們沒有的其他本地化上下文引導他們。 如果用戶登陸時沒有設置本地化設置,我們會將其設置為英語,因為這是我們網站的主要語言。 我們的頁腳中還有一個語言切換器,允許用戶更改他們的語言。 此時,我們的 service worker 應該已經安裝好了。
  • 安裝 Service Worker 後,我們會攔截所有用於站點導航的 URL 請求。 因為我們的本地化是基於子目錄的,所以我們可以很容易地確定正在請求什麼本地化。 一旦確定,我們檢查請求的頁面是否在本地化子目錄中,檢查本地化子目錄是否在支持的本地化列表中,並檢查本地化子目錄是否與存儲在 IndexedDB 中的首選項匹配。 如果它不在本地化子目錄中或本地化子目錄與他們的偏好匹配,我們提供頁面; 否則,我們會從我們的服務人員那裡進行 302 重定向,以獲得正確的本地化。

我們將我們的解決方案捆綁到 Workbox 插件 Service Worker Internationalization Redirect 中。 當與 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,他們將重定向。

調整站點用戶界面

正確調整用戶界面有很多內容,因此雖然這裡不會涵蓋所有內容,但可以而且應該以編程方式管理一些更微妙的事情。

塊報價行情

一種常見的設計模式是將塊引號括在引號中,但是您知道這些引號所使用的內容因本地化而異嗎? 代替硬編碼,使用open-quoteclose-quote來確保正確的引號用於正確的語言。

樣式指南中的塊引用,在帶有 lang=”en” 的頁面上,對開頭和結尾的引號使用開引號、閉引號
lang=“en”open-quoteclose-quote顯示為兩個上標逗號,它們向內朝向文本,第一對倒置。 (大預覽)
我們的風格指南中的塊引用,在帶有 lang=”fr” 的頁面上,對開頭和結尾的引號使用開引號、閉引號
lang=“fr”open-quoteclose-quote顯示為一對 V 形,它們的開口向內朝向文本。 (大預覽)

日期和數字格式

日期和數字都有一個方法, .toLocaleString允許基於本地化(語言和/或區域)進行格式化。 支持這些的瀏覽器附帶所有可用的本地化版本,使其在此處易於使用,但 Node.js 沒有。 幸運的是,Node 的完整 icu 模塊允許您使用所有可用的本地化數據。 為此,在安裝模塊後,運行代碼並將NODE_ICU_DATA環境變量設置為模塊的路徑,例如NODE_ICU_DATA=node_modules/full-icu

HTML 元信息

您的 HTML 標記和標題中的三個區域應隨每次本地化而更新:

  • 頁面的語言,
  • 寫作方向,
  • 頁面可用的替代語言。

第一個分別使用dirlang屬性的html元素,例如美國英語的<html lang="en" dir-"ltr"> 。 正確設置這些將確保內容朝著正確的方向流動,並且可以讓瀏覽器了解頁面的語言,從而允許翻譯內容等附加功能。 您還應該包含rel="alternate"鏈接,讓搜索引擎知道頁面已完全翻譯,因此在我們的英文著陸頁上包含<link href="/es" rel="alternate" hreflang="es">將讓搜索引擎知道這有一個翻譯它應該在尋找。

內在設計

本地化內容可能會帶來設計挑戰,因為不同的翻譯將在頁面上佔用不同數量的空間。 某些語言(例如德語)的單詞較長,需要更多的水平空間或更寬泛的文本換行。 其他語言,如阿拉伯語,具有更高的字體,需要更多的垂直空間。 幸運的是,有許多 CSS 工具可以使間距和佈局不僅響應視口大小,還響應內容,這意味著它們可以更好地適應多種語言。

有許多專門為處理內容而設計的 CSS 單元。 emrem單位分別代表計算的字體大小和根字體大小。 為這些單位交換固定大小的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 或審查來執行它們,您能夠確保本地化可以專注於本地化。

本地化值,而不是鍵

內容和微拷貝模型通常會生成類似於代碼庫中對象的結構; 無論是數據庫條目、JSON 對象、YAML 還是 Front Matter。 不要本地化對象鍵! 如果您的搜索文本微副本位於microcopy的微microcopy.search.text對像中,請不要將其放入microcopie的微microcopie.chercher.texte對像中。 模塊中的鍵應被視為與本地化無關的標識符,以便它們可以可靠地用於可重用模板並在整個代碼庫中依賴。 這也意味著對象鍵不應該作為內容或微副本顯示給最終用戶。

靜態站點設置

對於 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 代碼庫中,我們也經常將它們稱為locales 。 這些文件夾將成為本地化子目錄,將內容分段為本地化。 11ty 的數據級聯允許數據可用於目錄中的每個文件及其子文件,前提是該文件位於目錄的根目錄並與目錄命名相同(稱為目錄數據文件)。 11ty 使用從該文件返回的對象,或返回對象的函數,並將其註入可用於模板的變量中,因此我們可以在此處訪問該本地化的所有內容的數據。

為了幫助這些文件的可維護性,我們編寫了一個名為l10n-data的幫助程序,它是我們靜態站點腳手架的一部分,它利用此文件夾結構來構建級聯的本地化數據,從而允許數據零散地本地化。 它通過將數據存儲在特定於語言環境的數據目錄中來實現這一點,其中的_data目錄(加載到目錄數據文件中)。 例如,如果您查看我們的英語語言環境數據目錄,您會看到諸如locale.json之類的微拷貝模型,它定義了語言代碼和編寫方向,然後將呈現到我們的 HTML 中, newsletter.yml定義了我們所需的微拷貝時事通訊註冊,以及一個microcopy.yml文件,其中包括在整個站點的多個位置使用的一般微文案,這些文件不適合更具體的文件。 在任何使用此微副本的任何地方,我們都會從通過 11ty 將數據變量注入到我們的模板中以供使用的可用數據中提取它。

微文案往往是最難管理的,而其餘內容大多是直截了當的。 將您的內容(通常是 Markdown 文件或 HTML)放入本地化的子文件夾中。 對於處理文件夾結構的靜態站點生成器,內容的文件名和文件夾結構通常會 1:1 映射到該內容的最終 URL,因此位於en/web/pwas.md的 Markdown 文件將輸出到 URL en/web/pwa 。 遵循我們的“值,而不是鍵”本地化原則,我們決定不本地化內容文件名(以及路徑),這樣我們更容易跨區域跟踪同一文件的本地化狀態並讓用戶知道它們位於不同語言環境之間的正確頁面上。

I18n 助手

除了內容和微文案之外,我們發現我們還需要編寫一些幫助模塊,以便更輕鬆地處理本地化內容。 11ty 有一個稱為過濾器的概念,它允許在渲染之前修改內容。 我們最終構建了其中的四個來幫助 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 ,其中包含l10n屬性,其中包含來自{{locale-code}}.11tydata.js的所有微拷貝和本地化特定內容。 它還包含一個以數組形式列出所有可用語言環境的languages屬性。 最後,該函數輸出一個 JavaScript 文件,詳細說明站點支持哪些語言以及{{locale-code}}.11tydata.js中每個條目的各個文件,每個本地化都鍵入,所有這些都設計為由我們的瀏覽器腳本導入。 該文件的繁重工作將我們的靜態站點與我們的前端 JavaScript 聯繫在一起,唯一的事實來源是我們已經需要的本地化信息。 它還允許我們通過循環site.l10n以編程方式基於我們的本地化生成頁面。 這與我們的本地化特定集合相結合,讓我們使用 11ty 的分頁來創建本地化的主頁和新聞登錄頁面,而無需為每個頁面維護單獨的 HTML 頁面。

結論

正確實現國際化和本地化可能很困難; 了解不同的策略如何影響複雜性對於簡化它至關重要。 選擇一個自然適合靜態站點、子目錄的 i18n 策略,然後構建工具以從正在生成的內容中自動化 i18n 和 i10n 的部分內容。 構建強大的內容和縮微模型。 利用服務工作者進行與服務器無關的本地化。 將這一切與不僅響應屏幕尺寸而且響應內容的設計結合在一起。 最後,您將擁有一個所有語言環境的用戶都會喜歡的站點,該站點可以由作者和翻譯人員維護,就好像它是一個簡單的單一語言環境站點一樣。