PHP: The Right Way 国际化与本地化实战:基于 Gettext 的 PHP 多语言应用完整指南 文档教程【免费下载链接】php-the-right-wayAn easy-to-read, quick reference for PHP best practices, accepted coding standards, and links to authoritative tutorials around the Web项目地址https://gitcode.com/gh_mirrors/ph/php-the-right-way点击查看免费下载国际化Internationalization简称 i18n与本地化Localization简称 l10n是 PHP 应用走向多语言、多区域市场的必经之路。本文以 PHP: The Right Way 开源仓库中的 Internationalization and Localization 章节 为核心骨架系统讲解从概念辨析、工具选型、Gettext 安装配置到 PO/MO 文件体系、复数规则、Poedit 工作流与常见坑点的完整链路并结合仓库内 UTF-8 处理、数据过滤 等相邻章节补充源码级佐证让读者读完即可在自己的 PHP 项目中落地一套可维护的 i18n/l10n 方案。先厘清三个核心概念i18n、l10n 与复数规则新手提示i18n 和 l10n 都是 numeronym数字缩写词——用数字代替单词中省略的字母internationalization 缩写为 i18n首尾字母 i、n 之间有 18 个字母localization 缩写为 l10n首尾字母 l、n 之间有 10 个字母。要正确实施国际化必须先区分两个相似概念以及一个常被忽视的关联概念国际化Internationalization指把代码组织成可以适应不同语言或区域而无需重构的形式。这项工作通常只做一次——最好在项目启动之初就完成否则后期可能需要对源码进行大规模修改。本地化Localization在既有 i18n 成果的基础上主要通过翻译内容来适配界面。它通常在每次需要支持新语言或新区域时执行每当界面新增部件都需要为所有已支持的语言同步更新。复数规则Pluralization定义不同语言之间如何将「包含数字与计数器的字符串」互相衔接。例如英语中只有单数和复数两种形态多数单词加 S 即构成复数而俄语、塞尔维亚语在单数之外还有两种复数形式斯洛文尼亚语、爱尔兰语、阿拉伯语甚至存在四、五、六种形式。复数规则直接决定翻译文件需要为同一句子准备多少个翻译版本。常见的实现方式从数组文件到 Gettext数组文件方案简单但不可扩展国际化 PHP 软件最容易的方式是使用数组文件并在模板中直接引用这些字符串例如h1?$TRANS[title_about_page]?/h1但这种做法很难推荐给正经项目因为随着项目增长会暴露出一系列维护问题——有些问题在初期就会显现比如复数规则的处理。所以如果你的项目会超过几个页面请不要再尝试这种方案。其他 i18n 库与框架方案概览除了 PHP 核心自带的 Gettext 实现社区还有一些常见库有的支持 Gettext 之外的其他 i18n 文件格式有的提供额外特性。原文将其归纳如下库 / 框架格式支持特点提取器aura/intl数组格式消息按 locale 分包的消息翻译依赖intl扩展提供高级消息格式化含复数消息无php-gettext/Gettext.po/.mo及多种格式面向对象接口强大的多格式提取器含gettext命令原生不支持的格式可导出到其他格式以对接 JS 界面等系统部件有symfony/translation多种格式推荐 XLIFF内部用strtr()实现占位符不提供辅助函数与内置提取器无laminas/laminas-i18n数组、INI、Gettext内置缓存层避免每次读取文件系统含视图辅助函数、locale 感知的输入过滤器与验证器无Laravel框架内置基本数组文件提供模板辅助函数lang无自动提取器Yii框架内置数组、Gettext、数据库基于 PHP 5.3 起可用的Intl扩展依托 ICU 项目支持数字拼写、日期/时间/区间/货币/序数的高级格式化有一个重要的实战建议如果你选用的库没有提取器请坚持使用 gettext 文件格式这样你仍然可以借用原版 gettext 工具链包括 Poedit完成字符串提取与翻译也就是本文后续描述的那套工作流。Gettext历史、安装与启用为什么是 Gettext最经典、也常被业界当作 i18n/l10n 参考标杆的实现是 Unix 工具Gettext。它诞生于 1995 年至今仍是软件翻译的完整实现——既足够简单、容易跑起来又拥有强大的配套工具。本章节即围绕 Gettext 展开并会介绍一个图形化应用Poedit帮助你不必在命令行中手忙脚乱地维护 l10n 源文件。安装与启用扩展你需要通过包管理器安装 Gettext 及对应的 PHP 库例如apt-get或yum。安装完成后在php.ini中启用扩展Linux/Unixextensiongettext.soWindowsextensionphp_gettext.dll此外本章节使用Poedit来创建翻译文件。它通常出现在系统包管理器中支持 Unix、macOS 和 Windows 全平台也可从其官网免费下载。Gettext 文件体系POT / PO / MO 三种文件使用 Gettext 通常需要处理三种文件文件类型全称作用POPortable Object可移植对象可读的「已翻译对象」列表即给人阅读和编辑的翻译源文件MOMachine Object机器对象对应的二进制文件由 Gettext 在执行本地化时解释使用POTTemplate模板文件只包含源码中所有现存键作为生成和更新各 PO 文件的指南几点关键规则POT 并非强制取决于你使用的 l10n 工具只保留 PO/MO 文件也完全可以每种语言和区域对应一对 PO/MO 文件而每个**域domain**只有一个 POT。域Domains为同一词语的不同语义分家在大型项目中可能存在「同一个词在不同上下文传递不同含义」的情况此时需要把翻译拆分成不同的域domain。域本质上就是一组有名字的 POT/PO/MO 文件文件名即为该翻译域的名字。例如在 Symfony 项目中域被用来隔离「校验错误消息」的翻译。小型和中型项目为了简单起见通常只用一个域域的名字可以任意取——本文示例统一使用main。区域代码Locale codeISO 标准与方言Locale 是标识某一语言版本的代码遵循ISO 639-1语言与ISO 3166-1 alpha-2国家/地区规范两个小写字母表示语言可选地加下划线和两个大写字母表示国家或区域代码少数稀有语言使用三个字母。对某些使用者来说国家部分看似冗余但实际上很有必要——同一语言在不同国家存在方言例如奥地利德语de_AT、巴西葡萄牙语pt_BR。当国家部分缺失时该 locale 被视为该语言的「通用/混合」版本。目录结构规范Gettext 约定的落盘方式使用 Gettext 必须遵循特定的目录结构。首先在源码仓库中选定一个任意的 l10n 文件根目录在其内部为每个需要的 locale 建一个文件夹每个文件夹里再放一个固定的LC_MESSAGES目录用于存放所有 PO/MO 文件对project root ├─ src/ ├─ templates/ └─ locales/ ├─ forum.pot ├─ site.pot ├─ de/ │ └─ LC_MESSAGES/ │ ├─ forum.mo │ ├─ forum.po │ ├─ site.mo │ └─ site.po ├─ es_ES/ │ └─ LC_MESSAGES/ │ └─ ... ├─ fr/ │ └─ ... ├─ pt_BR/ │ └─ ... └─ pt_PT/ └─ ...观察上面示例forum和site是两个域因此de/LC_MESSAGES/下各有一对 PO/MO每个 localede、es_ES、fr、pt_BR、pt_PT都拥有自己独立的目录树。这正是前文「每语言一对 PO/MO、每域一个 POT」规则的落地形态。复数规则Plural Forms让每种语言各得其所正如引言所述不同语言的复数规则千差万别。Gettext 又一次帮我们解决了这个麻烦在创建新的.po文件时你需要为该语言声明复数规则对复数敏感的翻译条目会为每条规则提供一个不同的翻译形式。代码中调用 Gettext 时只需给出句子相关的数字Gettext 会自动计算应使用哪个形式——必要时还会做字符串替换。复数规则由两部分组成可用的复数形式数量nplurals和一个以n为变量的布尔测试plural该测试决定给定的数字落入哪条规则计数从 0 开始。原文给出的三个例子日语nplurals1; plural0—— 只有一种规则日语不区分单复数英语nplurals2; plural(n ! 1);—— 两种规则n为 1 时用第一条否则用第二条巴西葡萄牙语nplurals2; plural(n 1);—— 两种规则n大于 1 时用第二条否则用第一条在实际项目中你可以从公开的复数规则清单中复制所需语言的规则而不必手动编写。当代码中调用 Gettext 处理带计数器的句子时你必须同时传入相关数字Gettext 会判断当前应生效的规则并选用正确的本地化版本。相应地.po文件中必须为每条已定义的复数规则各准备一个句子。PO 文件实战示例从表头到复数翻译下面是一段.po文件节选——先不必纠结其格式细节而是关注整体内容后面会介绍如何轻松编辑它msgid msgstr Language: pt_BR\n Content-Type: text/plain; charsetUTF-8\n Plural-Forms: nplurals2; plural(n 1);\n msgid We are now translating some strings msgstr Nós estamos traduzindo algumas strings agora msgid Hello %1$s! Your last visit was on %2$s msgstr Olá %1$s! Sua última visita foi em %2$s msgid Only one unread message msgid_plural %d unread messages msgstr[0] Só uma mensagem não lida msgstr[1] %d mensagens não lidas逐段解读文件头Headermsgid与msgstr均为空字符串描述文件编码、复数规则等元信息。注意这里声明了Plural-Forms: nplurals2; plural(n 1);与上文的巴西葡萄牙语规则完全对应简单字符串翻译将英文句子直接翻译成巴西葡萄牙语带替换符的翻译借助sprintf的替换能力%1$s、%2$s译文可嵌入用户名与访问日期复数形式翻译英文侧给出单数msgid与复数msgid_plural两个源串葡萄牙语侧用msgstr[0]和msgstr[1]分别对应两条复数规则并通过%d在句子中直接显示数字。要点复数形式始终有两个msgid单数与复数因此建议不要使用语法复杂的语言作为翻译的源语言source of translation。翻译键l10n keys的两种流派之争你可能已经注意到本文示例直接把英文原句作为源 IDmsgid。这个msgid在所有.po文件中保持一致——其他语言的文件格式相同、msgid字段相同只是msgstr被翻译。关于翻译键业界主要有两种「学派」流派一msgid就是真实句子主要优势如果软件在某些语言下存在未翻译的片段屏幕上显示的键仍然保留含义——例如你从英语熟练翻译到西班牙语、但法语还需要帮助可以先发布缺法语句子的新页面网站未翻译部分会显示英文而不是乱码翻译者更容易理解上下文基于msgid做出准确翻译免费获得「源语言」的本地化源语言本身无需翻译唯一劣势如果后续需要修改实际文案必须跨多个语言文件替换相同的msgid。流派二msgid是结构化唯一键用结构化方式描述句子在应用中的角色通常包含其所在的模板或部件位置如top_menu.welcome而非句子内容优点代码组织更清晰文本内容与模板逻辑解耦缺点翻译者可能因缺少上下文而出错需要一份源语言文件如en.po作为其他语言翻译的基础缺失翻译时会显示无意义的键例如在未翻译的法语页面上显示top_menu.welcome而非「Hello there, User!」。这能倒逼翻译在发布前完整——但坏处是翻译问题会在界面上暴露得非常扎眼。部分库提供了「fallback回退」语言选项行为与流派一类似。权威倾向Gettext 官方手册总体偏好第一种流派——它通常对翻译者和用户在出问题时更友好本文也采用该方式而 Symfony 文档则偏好基于关键字的翻译以便所有翻译可以独立变更而不影响模板。日常使用一个完整的五步实战闭环把以上理论串联起来原文给出了从模板到上线的完整流程。核心思想是在页面中书写静态文本时调用 Gettext 函数 → 这些句子进入.po文件 → 翻译后编译为.mo→ Gettext 在渲染界面时按当前 locale 取用。第 1 步在模板中调用 Gettext 函数?php include i18n_setup.php ? div idheader 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()允许在单次调用中覆盖域domain——域配置见下一步。第 2 步编写i18n_setup.php设置文件这个文件负责选定正确的 locale 并完成 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); //its stored in a cookie so it can be reused } elseif (isset($_COOKIE[lang]) valid($_COOKIE[lang])) { // if the cookie is present instead, lets 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!); ?逐行拆解这套配置的职责代码作用valid($locale)白名单校验仅接受项目已支持的 locale注意这不是输入净化的替代品——原文代码注释反复强调you should sanitize this!这与仓库 数据过滤章节 中「永远不要信任外部输入」的原则一致$_GET[lang]→ cookie允许通过查询字符串切换语言并用 cookie 记忆用户选择HTTP_ACCEPT_LANGUAGE解析兜底策略按浏览器声明可接受的语言列表逐项匹配白名单将-规范化为_如en-US→en_US首个命中者即为当前语言putenv(LANG$lang)设置全局系统 locale供底层库感知setlocale(LC_ALL, $lang)影响日期函数LC_TIME、货币格式化LC_MONETARY等类别——这与仓库 Date and Time 章节 讨论的时间处理互为表里bindtextdomain(main, ../locales)绑定域main到翻译目录使 Gettext 查找../locales/lang/LC_MESSAGES/main.mobind_textdomain_codeset(main, UTF-8)声明文件读取编码——UTF-8 至关重要与仓库 UTF-8 章节 关于编码一致性mb_*函数、utf8mb4、mb_internal_encoding等的建议一脉相承textdomain(main)设置默认域使普通gettext()调用响应main域dgettext(forum, ...)注释演示单次调用切换到forum域的场景第 3 步用 Poedit 完成首次翻译准备Gettext 相对于框架内置 i18n 包的一大优势就是其强大而规范的文件格式。也许你会想「这格式太难手工编辑了一个数组不是更容易吗」——但像 Poedit 这样的应用就是来帮你解决这个问题的它免费、全平台可用、上手容易又足够强大能利用 Gettext 的全部特性。本指南基于 Poedit 1.8 编写。首次运行流程如下菜单选择File New...立即会被问到目标语言可选择/筛选要翻译的语言也可以直接用en_US或pt_BR这类格式按前文约定的目录结构保存文件点击Extract from sources从源码提取配置提取与翻译任务的各项设置——这些设置之后随时可以在Catalog Properties中找回Source paths源路径必须包含项目中所有调用gettext()及同族函数的目录通常是 templates/views 目录。这是唯一必填项Translation properties翻译属性项目名与版本、团队及团队邮箱写入.po文件头**Plural forms复数规则**填入上文所述的规则有示例链接大多数情况可保持默认——Poedit 内置了多语言的复数规则数据库**Charsets字符集**建议 UTF-8**Source code charset源码字符集**设为代码库所用的字符集通常也是 UTF-8Source keywords源关键字底层软件认识多种编程语言中gettext()及类似函数调用的样子但你也可以注册自己的翻译函数——这正是第 4 步的扩展点。配置完成后Poedit 会扫描源码找出所有本地化调用并显示发现/移除的摘要新条目以空值进入翻译表你在其中输入各字符串的本地化版本保存后.mo文件会在同一目录被重新编译——项目即完成国际化。第 4 步翻译字符串与持续维护正如前文所见本地化字符串主要有两类简单字符串只有「源字符串」和「本地化字符串」两个框。源字符串不可在 Poedit 中修改——Gettext/Poedit 没有能力改动你的源文件要改文案应改源码后重新扫描。提示右键点击某条翻译行Poedit 会提示该字符串在哪些源文件的哪些行被使用复数形式字符串显示两个源字符串框单数复数并以标签页形式让你配置不同复数规则下的最终形式。每当源码变更需要更新翻译时点击RefreshPoedit 会重新扫描代码移除已不存在的条目、合并变更的条目、新增条目。它还可能根据已有翻译猜测部分新翻译这些猜测以及变更过的条目会被打上Fuzzy模糊标记列表中以金色显示表示需要人工复查。这一机制对翻译团队协作尤其有用拿不准的翻译就标记 Fuzzy留给他人复核。最后建议始终勾选View Untranslated entries first先看未翻译条目这能极大帮助你避免遗漏任何条目同一菜单下还可以打开界面部件按需为翻译者留下上下文信息。第 5 步常见坑点与进阶技巧Tips Tricks可能的缓存问题如果你在 Apache 上以模块方式运行 PHPmod_php可能会遇到.mo文件被缓存的问题首次读取后缓存生效后续要更新翻译可能需要重启服务器。在 Nginx PHP5 下通常只需刷新几次页面即可更新翻译缓存PHP7 下则很少需要。自定义辅助函数与提取器配置很多人偏爱用_()代替gettext()框架的自定义 i18n 库也常提供类似t()的短函数让代码更简洁。但注意_()是唯一有官方别名的函数。你可以在项目中补充自己的辅助函数例如__()普通翻译_n()对应ngettext()的复数翻译_r()把gettext()与sprintf()串起来的「带替换的翻译」。一旦引入这些新函数就需要教给 Gettext 提取器如何从中抽取字符串。方法很简单只需在.po文件的一个字段中Poedit 里是Catalog Properties Source keywords按特定格式声明若创建的是像t()这样「唯一参数即待翻译字符串」的函数直接写t即可——Gettext 会知道唯一的函数参数就是要翻译的字符串若函数有多个参数则需指明第一个字符串在哪个参数位必要时还要指明复数形式的位置。例如调用__(one user, %d users, $number)声明为__:1,2表示第一个形式是第 1 个参数、第二个形式是第 2 个参数若数字放在第一个参数位如__($number, one user, %d users)声明则应写__:2,3。声明完毕后重新扫描新函数中的字符串就会像之前一样被自动提取进来。将 i18n/l10n 放回 PHP: The Right Way 的完整上下文本文所依据的章节是 PHP: The Right Way 仓库中「Coding Practices编码实践」部分的子章节。该仓库是一个 Jekyll 项目见 README.md 与 _config.yml每个章节对应_posts/下的一个 Markdown 文件子章节通过 front matter 中的isChild: true标识导航与页面结构自动生成见 index.html 与 _layouts/default.html。将 i18n/l10n 与仓库其他章节联动才能构成一套完整的多语言应用实践编码一致性PHP and UTF-8 章节 强调 PHP 底层不原生支持 Unicode必须全链路使用mb_*函数、显式声明编码mb_internal_encoding、mb_http_output、数据库采用utf8mb4——这与bind_textdomain_codeset(main, UTF-8)、PO 文件表头的charsetUTF-8是同一套「处处 UTF-8」纪律的不同侧面输入安全i18n 设置文件中的$_GET[lang]、$_COOKIE[lang]都是典型的外部输入Data Filtering 章节 的原则filter_var()/filter_input()校验、HTML 输出转义同样适用于 locale 参数——这也是原文档代码中反复注释you should sanitize this!的原因时间与货币本地化setlocale(LC_ALL, $lang)会影响日期与货币格式化与 Date and Time 章节 中DateTime、DateInterval的使用配合可实现真正的「区域感知」界面。结语从概念上区分 i18n/l10n/复数规则到工具选型时认清数组方案的边界再到 Gettext 的 POT/PO/MO 文件体系、域与 locale 规范、目录结构、复数规则、PO 文件写法、翻译键流派之争最后落到gettext()/ngettext()/dgettext()调用、i18n_setup.php完整配置与 Poedit 的扫描—翻译—编译—刷新闭环——这套方法论足以支撑一个 PHP 项目从单语言走向多语言、多区域。实践中的两个核心纪律值得反复强调locale 输入同样需要净化衔接仓库的数据过滤章节编码全程保持 UTF-8衔接仓库的 UTF-8 章节。基于此再配合你选定的 i18n 库或 Gettext 工具链即可稳健地管理日益增长的翻译资产。如需继续深入可在本仓库中对照阅读Internationalization and Localization 原文、PHP and UTF-8、Date and Time、Data Filtering以及 Components 章节 中列举的社区 i18n 组件。赞分享文档教程【免费下载链接】php-the-right-wayAn easy-to-read, quick reference for PHP best practices, accepted coding standards, and links to authoritative tutorials around the Web项目地址https://gitcode.com/gh_mirrors/ph/php-the-right-way点击查看免费下载相关推荐Wordless高级设置指南自定义分析流程的10个技巧Wordless高级设置指南自定义分析流程的10个技巧 Wordless是一款功能强大的多语种语料库分析工具专为语言、文学和翻译研究设计。这款开源软件支持超PHP国际化本地化终极指南时区处理与多语言支持完整实现PHP国际化本地化终极指南时区处理与多语言支持完整实现 在当今全球化的互联网环境中PHP国际化本地化开发已成为构建成功Web应用的必备技能。无论是面向多语言知识库yaml-cpp多语言本地化终极指南使用Gettext实现国际化支持yaml cpp多语言本地化终极指南使用Gettext实现国际化支持 yaml cpp是一个强大的C YAML解析器和发射器库支持YAML 1.2规范。序列化后端上一篇终极指南如何将GLM-4模型从PyTorch无缝迁移到TensorFlow下一篇三分钟搞定Java多版本冲突jenv环境管理实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考