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. 可能會轉換成 Y?ú'?è ?è?è??èδ 3 Tá?è Rú?è?。

偽本地化模式還一個便利功能。您可以將所有字符串縮進或拉長一個固定的量,從而為內容寬度差異做好準備。以下是長度拉長50% 后的同一偽本地化句子:Y?ú'?è ?è?è??èδ 3 Tá?è 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> 組件及其功能?,F在,我們來談談它的構建方式。

或許不足為奇,我們不想徒勞無功,從頭開始提供基本的 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",
 // ...
}

在這里,我們有逐字照搬的字符串,也有拷貝的子字符串。翻譯服務是按單詞數收費的。誰也不想支付兩次費用,并且面臨出現一致性問題的風險。為此,擁有一個維護良好的翻譯記憶庫能確保這些字符串在預翻譯步驟中得到妥善處理,甚至是在翻譯人員看到文件之前。

翻譯工作標記為準備就緒后,翻譯團隊可能需要數小時到數周不等的時間來完成翻譯工作,具體取決于諸多因素,如工作量大小、翻譯人員閑忙情況,以及相關的合同條款。此階段的關注點或可寫成另一篇類似長度的博文:尋找合適的翻譯團隊、控制成本、保證質量和一致性,確保正確傳達公司品牌形象,等等。本文的重點主要在于技術,我們就先忽略這些細節了。但請不要誤解,這一部分出錯會讓您前功盡棄,即使您的技術目標已經達成。

翻譯團隊發出新文件準備就緒的信號后,相關的資產會從服務器中提取出來并解壓到應用程序代碼中的正確位置。然后,我們運行一套自動化檢查,以確保所有文件均有效且沒有任何格式問題。

這個階段要完成一個可選(但強烈建議的)步驟 — 上下文審閱。一組翻譯審閱者會在上下文中查看翻譯后的輸出,確保一切在最終狀態下看起來盡善盡美。開展這項工作時,擁有既精通產品又能熟練使用目標語言的支持人員會特別有幫助。向公司中所有花費時間和精力來做這件事的團隊成員大聲道謝。為了使外部承包商可以承擔此工作,我們準備了應用程序的特殊預覽版本,讓他們能夠在啟用開發模式區域設置的情況下進行測試。

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

持續本地化

這里應該可以停止了,但到此刻為止,我們討論的都只是一次所需的工作。眾所周知,代碼會發生變化。隨著新功能的發布和調整,不時會有新字符串逐漸添加、修改和刪除。

翻譯在很大程度上需要人類介入,常常涉及來自全球不同角落的人的投入,因此可行的更新周期會有一個下限。由于我們的發布節奏(每天)通??煊诖烁骂l率(2-5 天),這意味著對功能進行更改的開發人員必須做出選擇:減慢速度以匹配這種節奏,或者比本地化進度稍微提前一些交付而不完全覆蓋。

為了確保翻譯之前交付的功能不會導致應用程序中斷錯誤,我們會在配置的語言中沒有某個字符串時使它回退到基本區域設置(en_US)。

一些應用程序的回退行為略有不同:顯示原始翻譯鍵(也許您在使用的應用中看到過 some.funny.dot.delimited.string)。這里要在速度和正確性之間權衡,而我們選擇針對速度和最小開銷進行優化。在某些應用中,正確性的重要程度足以減慢 i18n 的節奏,而我們并未發生這種情況。

盡善盡美

在我們新近本地化的應用程序中,還可以做些事情來優化用戶體驗。

首先,確保沒有任何性能衰退。如果應用程序讓用戶在呈現頁面之前獲取所有翻譯字符串,則肯定會發生這種衰退。因此,為了使所有內容順暢運行,僅當應用程序需要在頁面上呈現某些內容時,才異步獲取相應翻譯目錄。如今這很容易實現,借助支持動態導入語句的模塊捆綁器中提供的代碼拆分功能便可,如 ParcelWebpack。

我們還希望消除用戶在訪問不同 Cloudflare 資產時必須不斷選擇所需語言所帶來的不暢。為此,我們確保用戶在我們營銷網站支持網站上做出的任何語言偏好選擇會在瀏覽到儀表板或離開之時依然保留(為了充分說明這一點,所有鏈接都是法語鏈接)。

下一步是什么?

這是一段精彩紛呈的旅程,其過程帶給我們很多收獲。很難(也許不可能)說一個 i18n 項目真正宣告結束。擴展到新語言時會冒出狡猾的錯誤,暴露新的挑戰。預算壓力會迫使您尋找削減成本和提升效率的方法。此外,您還會發現可以為用戶進一步增強本地化體驗的方法。

我們有許多方面需要改進,但是這里有一些要點:

  • 整理。字符串比較是語言敏感的;因此,如果編寫的代碼按字典順序對應用程序中的列表和數據表排序,那么對某些用戶來說可能是不適合的。這一點在使用語標文字系統的語言(如中文或日語)中體現的尤為明顯,它們與使用字母的語言(如英語或西班牙語)截然不同。
  • 支持從右到左的語言,如阿拉伯語和希伯來語。
  • 本地化 API 響應比本地化用戶界面中的靜態文字更加困難,因為這需要不同團隊之間的協調配合。在微服務時代,要找到一種在為各種服務提供支持的無數技術堆棧中都得心應手的解決方案,是非常具有挑戰性的。
  • 本地化地圖。我們將努力確?;诘貓D的可視化呈現中的所有內容都得到翻譯。
  • 機器翻譯在最近幾年有了很大發展,但還不足以在無人監督前提下攪動我們的翻譯。不過,我們希望進行更多的嘗試,將機器翻譯作為第一階段,然后由翻譯審閱人員進行編輯以確保正確性和基調。

希望您喜歡這篇關于 Cloudflare 如何國際化和本地化我們儀表板的概述。歡迎您訪問我們的事業發展頁面,了解有關全球全職職位和實習崗位的更多信息。