如何構建多語言應用程序:使用 PHP 和 Gettext 的演示
已發表: 2022-03-11無論您是構建網站還是成熟的 Web 應用程序,要讓更廣泛的受眾可以訪問它,通常需要它以不同的語言和區域設置可用。
大多數人類語言之間的根本差異使這絕非易事。 語法規則、語言細微差別、日期格式等方面的差異使本地化成為一項獨特而艱鉅的挑戰。
考慮這個簡單的例子。
英語中的複數規則非常簡單:你可以有一個單詞的單數形式或一個單詞的複數形式。
然而,在其他語言中——例如斯拉夫語言——除了單數形式外,還有兩種複數形式。 您甚至可以找到總共有四種、五種或六種複數形式的語言,例如斯洛文尼亞語、愛爾蘭語或阿拉伯語。
代碼的組織方式以及組件和界面的設計方式對於確定應用程序本地化的難易程度起著重要作用。
代碼庫的國際化 (i18n) 有助於確保它可以相對輕鬆地適應不同的語言或地區。 國際化通常只進行一次,最好是在項目開始時進行,以避免以後需要對源代碼進行巨大更改。
一旦您的代碼庫國際化,本地化(l10n) 就變成了將應用程序的內容翻譯成特定語言/區域設置的問題。
每次需要支持新的語言或地區時,都需要執行本地化。 此外,每當界面的一部分(包含文本)被更新時,新的內容就會變得可用——然後需要將其本地化(即翻譯)到所有支持的語言環境。
在本文中,我們將學習如何對用 PHP 編寫的軟件進行國際化和本地化。 我們將通過各種實施選項和我們可以使用的不同工具來簡化流程。
國際化工具
使 PHP 軟件國際化的最簡單方法是使用數組文件。 數組將填充翻譯後的字符串,然後可以從模板中查找:
<h1><?=$TRANS['title_about_page']?></h1>
然而,對於嚴肅的項目來說,這幾乎不是推薦的方式,因為它肯定會在未來造成維護問題。 有些問題甚至可能在一開始就出現,例如缺乏對變量插值或名詞複數的支持等。
最經典的工具之一(通常作為 i18n 和 l10n 的參考)是一個名為 Gettext 的 Unix 工具。
雖然可以追溯到 1995 年,但它仍然是一款易於使用的翻譯軟件的綜合工具。 雖然它很容易上手,但它仍然具有強大的支持工具。
Gettext 是我們將在這篇文章中使用的。 我們將展示一個出色的 GUI 應用程序,可用於輕鬆更新您的 l10n 源文件,從而避免處理命令行的需要。
圖書館讓事情變得簡單
有主要的 PHP Web 框架和庫支持 Gettext 和 i18n 的其他實現。 有些比其他更容易安裝,或者俱有附加功能或支持不同的 i18n 文件格式。 儘管在本文檔中,我們關注的是 PHP 核心提供的工具,但這裡列出了一些其他值得一提的工具:
oscarotero/Gettext:具有面向對象接口的 Gettext 支持; 包括改進的輔助函數、多種文件格式的強大提取器(其中一些不被
gettext
命令本機支持)。 還可以導出為 .mo/.po 文件以外的格式,如果您需要將翻譯文件集成到系統的其他部分(如 JavaScript 界面)中,這將非常有用。symfony/translation:支持很多不同的格式,但建議使用詳細的 XLIFF。 不包括輔助函數或內置提取器,但支持在內部使用
strtr()
佔位符。zend/i18n:支持數組和 INI 文件,或 Gettext 格式。 實現緩存層以避免每次都需要讀取文件系統。 還包括視圖助手、區域感知輸入過濾器和驗證器。 但是,它沒有消息提取器。
其他框架也包括 i18n 模塊,但這些模塊在其代碼庫之外不可用:
Laravel:支持基本數組文件; 沒有自動提取器,但包含用於模板文件的
@lang
幫助器。Yii:支持數組、Gettext 和基於數據庫的翻譯,並包含消息提取器。 由
Intl
擴展支持,自 PHP 5.3 起可用,並基於 ICU 項目。 這使 Yii 能夠運行強大的替換功能,例如拼寫數字、格式化日期、時間、間隔、貨幣和序數。
如果您決定選擇不提供提取器的庫之一,您可能希望使用 Gettext 格式,因此您可以使用本章其餘部分所述的原始 Gettext 工具鏈(包括 Poedit)。
安裝 Gettext
您可能需要使用包管理器(如 apt-get 或 yum)安裝 Gettext 和相關的 PHP 庫。 安裝後,通過將extension=gettext.so
(Linux/Unix) 或extension=php_gettext.dll
(Windows) 添加到您的php.ini
文件來啟用它。
在這裡,我們還將使用 Poedit 創建翻譯文件。 您可能會在系統的包管理器中找到它; 它適用於 Unix、Mac 和 Windows,也可以在其網站上免費下載。
Gettext 文件的類型
在使用 Gettext 時,您通常會處理三種文件類型。
主要是 PO(便攜式對象)和 MO(機器對象)文件,第一個是可讀的“翻譯對象”列表,第二個是相應的二進製文件(在進行本地化時由 Gettext 解釋)。 還有一個 POT(PO 模板)文件,它只包含源文件中的所有現有密鑰,可用作生成和更新所有 PO 文件的指南。
模板文件不是強制性的; 根據您用於執行 l10n 的工具,您只需使用 PO/MO 文件就可以了。 對於每種語言和地區,您將擁有一對 PO/MO 文件,但每個域只有一個 POT。
分離域
在某些情況下,在大型項目中,當相同的詞在不同的上下文中傳達不同的含義時,您可能需要分開翻譯。
在這些情況下,您需要將它們拆分為不同的“域”,這些“域”基本上是命名為 POT/PO/MO 文件的組,其中文件名是所述翻譯域。
為簡單起見,中小型項目通常只使用一個域; 它的名稱是任意的,但我們將在代碼示例中使用“main”。
例如,在 Symfony 項目中,域用於分隔驗證消息的翻譯。
語言環境代碼
語言環境只是標識一種語言版本的代碼。 它是根據 ISO 639-1 和 ISO 3166-1 alpha-2 規範定義的:語言的兩個小寫字母,可選地後跟一個下劃線和兩個大寫字母標識國家或地區代碼。
對於稀有語言,使用三個字母。
對於一些發言者來說,國家部分似乎是多餘的。 事實上,有些語言在不同國家有方言,例如奧地利德語(de_AT)或巴西葡萄牙語(pt_BR)。 第二部分用於區分這些方言——當它不存在時,它被視為語言的“通用”或“混合”版本。
目錄結構
要使用 Gettext,我們需要遵守特定的文件夾結構。
首先,您需要為源存儲庫中的 l10n 文件選擇任意根。 在其中,您將擁有每個所需語言環境的文件夾,以及一個固定的“LC_MESSAGES”文件夾,其中將包含您的所有 PO/MO 對。
複數形式
正如我們在介紹中所說,不同的語言可能具有不同的複數規則。 但是,Gettext 為我們省去了這個麻煩。
在創建新的 .po 文件時,您必須聲明該語言的複數規則,並且對複數敏感的翻譯片段對於每個規則都有不同的形式。
在代碼中調用 Gettext 時,您必須指定與句子相關的數字(例如,對於短語“您有 n 條消息。”,您需要指定 n 的值),它會計算出正確的形式使用 - 如果需要,甚至使用字符串替換。
複數規則由必要的規則數量組成,每個規則都有一個布爾測試(最多可以省略一個規則的測試)。 例如:
日語:
nplurals=1; plural=0;
nplurals=1; plural=0;
- 一條規則:沒有復數形式英語:
nplurals=2; plural=(n != 1);
nplurals=2; plural=(n != 1);
- 兩條規則:僅當 n 不為 1 時使用複數形式,否則使用單數形式。巴西葡萄牙語:
nplurals=2; plural=(n > 1);
nplurals=2; plural=(n > 1);
- 兩條規則,僅當 n 大於 1 時使用複數形式,否則使用單數形式。
如需更深入的解釋,請在線獲取內容豐富的 LingoHub 教程。
Gettext 將根據提供的數字確定要使用的規則,並將使用正確的本地化版本的字符串。 對於需要處理複數形式的字符串,您需要在 .po 文件中為定義的每個複數規則包含不同的句子。
示例實現
說了這麼多理論,讓我們來點實際的。 這是 .po 文件的摘錄(不要太擔心語法,只需了解整體內容即可):
msgid "" msgstr "" "Language: pt_BR\n" "Content-Type: text/plain; charset=UTF-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" msgid "We're now translating some strings" msgstr "Nos estamos traduzindo algumas strings agora" msgid "Hello %1$s! Your last visit was on %2$s" msgstr "Ola %1$s! Sua ultima visita foi em %2$s" msgid "Only one unread message" msgid_plural "%d unread messages" msgstr[0] "So uma mensagem nao lida" msgstr[1] "%d mensagens nao lidas"
第一部分像標題一樣工作, msgid
和msgstr
為空。
它描述了文件編碼、複數形式和其他一些內容。 第二部分將一個簡單的字符串從英語翻譯成巴西葡萄牙語,第三部分也是如此,但利用sprintf
的字符串替換,使翻譯能夠包含用戶名和訪問日期。
最後一部分是複數形式的示例,在英語中顯示單數和復數版本為msgid
,其對應的翻譯為msgstr
0 和 1(遵循複數規則給出的數字)。
在那裡,也使用了字符串替換,因此可以使用%d
直接在句子中看到數字。 複數形式總是有兩個msgid
(單數和復數),因此建議不要使用複雜的語言作為翻譯源。
本地化鍵
您可能已經註意到,我們使用實際的英文句子作為源 ID。 該msgid
在您的所有 .po 文件中使用相同,這意味著其他語言將具有相同的格式和相同的msgid
字段,但翻譯了msgstr
行。
說到翻譯鍵,這裡有兩種標準的“哲學”方法:
1.msgid作為真句
這種方法的主要優點是:
如果軟件的某些部分未翻譯成任何給定語言,則顯示的鍵仍將保持某些含義。 例如,如果您知道如何從英語翻譯成西班牙語,但在翻譯成法語時需要幫助,您可能會發布缺少法語句子的新頁面,而網站的部分內容將改為以英語顯示。
翻譯者更容易理解正在發生的事情並根據
msgid
進行適當的翻譯。它為您提供一種語言的“免費”l10n - 源語言。
另一方面,主要缺點是,如果您需要更改實際文本,則需要跨多個語言文件替換相同的msgid
。
2. msgid 作為唯一的結構化鍵
這將以結構化的方式描述應用程序中的句子角色,包括字符串所在的模板或部分,而不是其內容。
這是組織代碼的好方法,將文本內容與模板邏輯分開。 但是,這可能會給錯過上下文的翻譯人員帶來問題。
需要一個源語言文件作為其他翻譯的基礎。 例如,理想情況下,開發人員應該有一個“en.po”文件,翻譯人員會閱讀該文件以了解在“fr.po”中要寫什麼。
缺少翻譯會在屏幕上顯示無意義的鍵(“top_menu.welcome”而不是“Hello there,User!”在所述未翻譯的法語頁面上)。
這很好,因為它會強制翻譯在發布之前完成 - 但不好的是翻譯問題在界面中真的很糟糕。 但是,一些庫包含一個選項,可以將給定語言指定為“後備”,具有與其他方法類似的行為。
Gettext 手冊偏愛第一種方法,因為通常情況下,翻譯人員和用戶更容易遇到問題。 這也是我們將在這裡使用的方法。
不過應該注意的是,Symfony 文檔支持基於關鍵字的翻譯,以允許獨立更改所有翻譯而不影響模板。
日常使用
在一個常見的應用程序中,您將在頁面中編寫靜態文本時使用一些 Gettext 函數。
然後這些句子將出現在 .po 文件中,被翻譯,編譯成 .mo 文件,然後在渲染實際界面時由 Gettext 使用。 鑑於此,讓我們將到目前為止所討論的內容結合在一起,以一步一步的示例為例:
1. 一個示例模板文件,包括一些不同的 gettext 調用
<?php include 'i18n_setup.php' ?> <div> <h1><?=sprintf(gettext('Welcome, %s!'), $name)?></h1> <!-- code indented this way only for legibility → <?php if ($unread): ?> <h2> <?=sprintf( ngettext('Only one unread message', '%d unread messages', $unread), $unread )?> </h2> <?php endif ?> </div> <h1><?=gettext('Introduction')?></h1> <p><?=gettext('We\'re now translating some strings')?></p>
gettext()
簡單地將一個msgid
翻譯成它對應的給定語言的msgstr
。 還有速記函數_()
以同樣的方式工作ngettext()
做同樣的事情,但有復數規則還有
dgettext()
和dngettext()
,允許您為單個調用覆蓋域(下一個示例中有關域配置的更多信息)
2. 一個示例設置文件(如上所用的 i18n_setup.php),選擇正確的語言環境並配置 Gettext
使用 Gettext 涉及一些樣板代碼,但主要是關於配置語言環境目錄和選擇適當的參數(語言環境和域)。
<?php /** * Verifies if the given $locale is supported in the project * @param string $locale * @return bool */ function valid($locale) { return in_array($locale, ['en_US', 'en', 'pt_BR', 'pt', 'es_ES', 'es'); } //setting the source/default locale, for informational purposes $lang = 'en_US'; if (isset($_GET['lang']) && valid($_GET['lang'])) { // the locale can be changed through the query-string $lang = $_GET['lang']; //you should sanitize this! setcookie('lang', $lang); //it's stored in a cookie so it can be reused } elseif (isset($_COOKIE['lang']) && valid($_COOKIE['lang'])) { // if the cookie is present instead, let's just keep it $lang = $_COOKIE['lang']; //you should sanitize this! } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // default: look for the languages the browser says the user accepts $langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); array_walk($langs, function (&$lang) { $lang = strtr(strtok($lang, ';'), ['-' => '_']); }); foreach ($langs as $browser_lang) { if (valid($browser_lang)) { $lang = $browser_lang; break; } } } // here we define the global system locale given the found language putenv("LANG=$lang"); // this might be useful for date functions (LC_TIME) or money formatting (LC_MONETARY), for instance setlocale(LC_ALL, $lang); // this will make Gettext look for ../locales/<lang>/LC_MESSAGES/main.mo bindtextdomain('main', '../locales'); // indicates in what encoding the file should be read bind_textdomain_codeset('main', 'UTF-8'); // if your application has additional domains, as cited before, you should bind them here as well bindtextdomain('forum', '../locales'); bind_textdomain_codeset('forum', 'UTF-8'); // here we indicate the default domain the gettext() calls will respond to textdomain('main'); // this would look for the string in forum.mo instead of main.mo // echo dgettext('forum', 'Welcome back!'); ?>
3. 準備第一次運行的翻譯
Gettext 相對於自定義框架 i18n 包的一大優勢是其廣泛而強大的文件格式。
也許你在想“哦,天哪,這很難理解和手動編輯,一個簡單的數組會更容易!” 毫無疑問,像 Poedit 這樣的應用程序可以提供幫助 - 很多。 您可以從他們的網站獲取該程序,它是免費的,適用於所有平台。 這是一個非常容易習慣的工具,同時也是一個非常強大的工具 - 使用 Gettext 提供的所有功能。 我們將在這裡使用最新版本 Poedit 1.8。
在第一次運行時,您應該從菜單中選擇“文件 > 新建...”。 您將被要求提供語言; 選擇/過濾您要翻譯的語言,或使用我們之前提到的格式,例如en_US
或pt_BR
。
現在,保存文件 - 使用我們提到的目錄結構。 然後您應該單擊“從源中提取”,在這裡您將配置提取和翻譯任務的各種設置。 您稍後可以通過“目錄 > 屬性”找到所有這些:
源路徑:包括項目中調用
gettext()
(和兄弟姐妹)的所有文件夾 - 這通常是您的模板/視圖文件夾。 這是唯一的強制設置。翻譯屬性:
- 項目名稱和版本、團隊和團隊的電子郵件地址: .po 文件標題中的有用信息。
- 複數形式:這些是我們之前提到的規則。 大多數情況下,您可以將其保留為默認選項,因為 Poedit 已經包含一個方便的數據庫,其中包含許多語言的複數規則。
- 字符集:最好是 UTF-8。
- 源代碼字符集:您的代碼庫使用的字符集 - 也可能是 UTF-8,對吧?
源關鍵字:底層軟件知道
gettext()
和類似函數調用在幾種編程語言中的樣子,但您不妨創建自己的翻譯函數。 您將在這裡添加其他方法。 這將在後面的“提示”部分中討論。
設置這些屬性後,Poedit 將掃描您的源文件以查找所有本地化調用。 每次掃描後,Poedit 將顯示從源文件中找到的內容和刪除的內容的摘要。 轉換錶中的新條目將是空的,允許您輸入這些字符串的本地化版本。 保存它,一個 .mo 文件將被(重新)編譯到同一個文件夾中,並且,presto!,您的項目已國際化!
Poedit 還可以建議來自網絡和以前文件的常見翻譯。 它很方便,因此您只需檢查它們是否有意義並接受它們。 如果您不確定翻譯,可以將其標記為模糊,它將顯示為黃色。 藍色條目是那些沒有翻譯的條目。
4. 翻譯字符串
您可能已經註意到,本地化字符串有兩種主要類型:簡單字符串和復數形式的字符串。
簡單的只有兩個框:源和本地化字符串。 無法修改源字符串,因為 Gettext/Poedit 不包括更改源文件的功能; 相反,您需要更改源本身並重新掃描文件。 (提示:如果您右鍵單擊翻譯行,它將顯示源文件和正在使用該字符串的行的提示。)
複數形式的字符串包括兩個顯示兩個源字符串的框和選項卡,因此您可以配置不同的最終形式。
Poedit 上具有復數形式的字符串示例,顯示每個字符串的翻譯選項卡。
每當您更改源代碼文件並需要更新翻譯時,只需點擊刷新,Poedit 就會重新掃描代碼,刪除不存在的條目,合併已更改的條目並添加新條目。
Poedit 也可能會嘗試根據您所做的其他翻譯來猜測一些翻譯。 這些猜測和更改的條目將收到一個“模糊”標記,表明它們需要審查,在列表中以黃色顯示。
如果您有一個翻譯團隊並且有人試圖寫一些他們不確定的東西,這也很有用:只需將其標記為模糊,其他人稍後會對其進行審核。
最後,建議保留“查看 > 未翻譯的條目優先”標記,因為它可以幫助您避免忘記任何條目。 從該菜單中,您還可以打開 UI 的某些部分,以便您在需要時為翻譯人員留下上下文信息。
提示與技巧
Web 服務器最終可能會緩存您的 .mo 文件。
如果您在 Apache (mod_php) 上將 PHP 作為模塊運行,您可能會遇到 .mo 文件被緩存的問題。 它在第一次讀取時發生,然後,要更新它,您可能需要重新啟動服務器。
在 Nginx 和 PHP5 上,刷新翻譯緩存通常只需要幾次頁面刷新,而在 PHP7 上很少需要。
庫提供幫助函數來保持本地化代碼的簡短。
許多人喜歡使用_()
而不是gettext()
更容易。 許多來自框架的自定義 i18n 庫也使用類似於t()
的東西,以使翻譯後的代碼更短。 但是,這是唯一具有快捷方式的功能。
你可能想在你的項目中添加一些其他的東西,比如__()
或_n()
用於ngettext()
,或者可能是一個花哨的_r()
來加入gettext()
和sprintf()
調用。 其他庫,例如 oscarotero 的 Gettext 也提供了類似的輔助函數。
在這些情況下,您需要指導 Gettext 實用程序如何從這些新函數中提取字符串。 不要害怕,這很容易。 它只是 .po 文件中的一個字段或 Poedit 中的設置屏幕(在編輯器中,該選項位於“目錄 > 屬性 > 源關鍵字”中)。
請記住:Gettext 已經知道許多語言的默認函數,因此如果該列表看起來是空的,請不要擔心。 您需要按照以下特定格式在該列表中包含新功能的規範:
如果你創建類似
t()
的東西,它只是返回一個字符串的翻譯,你可以將它指定為t
。 Gettext 將知道唯一的函數參數是要翻譯的字符串;如果函數有多個參數,您可以指定第一個字符串在哪個參數中,如果需要,還可以指定複數形式。 例如,如果我們的函數簽名是
__('one user', '%d users', $number)
,規範將是__:1,2
,這意味著第一個形式是第一個參數,第二個形式是第二個論點。 如果您的數字作為第一個參數出現,則規範將為__:2,3
,表示第一種形式是第二個參數,依此類推。
在 .po 文件中包含這些新規則後,新的掃描將像以前一樣輕鬆地引入您的新字符串。
使用 Gettext 使您的 PHP 應用程序多語言
Gettext 是一個非常強大的工具,用於將您的 PHP 項目國際化。 除了支持大量人類語言的靈活性之外,它還支持 20 多種編程語言,讓您可以輕鬆地將使用 PHP 的知識轉移到 Python、Java 或 C# 等其他語言。
此外,Poedit 可以幫助平滑代碼和翻譯字符串之間的路徑,使過程更加直接和易於遵循。 它還可以通過其 Crowdin 集成來簡化共享翻譯工作。
只要有可能,請考慮您的用戶可能會說的其他語言。 這對於非英語項目最為重要:如果您以英語和您的母語發布它,您可以提高用戶訪問權限。
當然,並非所有項目都需要國際化,但在項目初期啟動 i18n(即使最初不需要)要比在以後成為要求時再做要容易得多。 而且,使用 Gettext 和 Poedit 等工具,它比以往任何時候都容易。