如何构建多语言应用程序(PHP和Gettext演示)

本文概述

  • 国际化工具
  • 样例实施
  • 日常使用
  • 提示与技巧
  • 使用Gettext使你的PHP应用成为多语言
无论是构建网站还是功能完善的Web应用程序, 要使其能够被更广泛的受众访问, 通常都需要以不同的语言和区域提供该网站。
大多数人类语言之间的根本差异使这一切变得简单。语法规则, 语言细微差别, 日期格式等方面的差异加在一起, 使本地化成为独特而艰巨的挑战。
考虑这个简单的例子。
英语中的复数规则非常简单:你可以使用单词的单数形式或单词的复数形式。
但是, 在其他语言中(例如斯拉夫语), 除单数形式外, 还有两种复数形式。你甚至可以找到总共有四种, 五种或六种复数形式的语言, 例如斯洛文尼亚语, 爱尔兰语或阿拉伯语。
代码的组织方式以及组件和接口的设计方式在确定本地化应用程序的容易程度方面起着重要作用。
代码库的国际化(i18n)有助于确保相对容易地将其适应于不同的语言或地区。国际化通常只执行一次, 最好在项目开始时进行, 以避免在源代码中进行大量更改。
如何构建多语言应用程序(PHP和Gettext演示)

文章图片
一旦你的代码库国际化, 本地化(l10n)就变成了将应用程序的内容转换为特定语言/语言环境的问题。
【如何构建多语言应用程序(PHP和Gettext演示)】每当需要支持新的语言或地区时, 都需要执行本地化。而且, 每当界面的一部分(包含文本)被更新时, 新的内容就变得可用-然后需要将其本地化(即翻译)为所有支持的语言环境。
在本文中, 我们将学习如何国际化和本地化用PHP编写的软件。我们将介绍各种实施选项和可供我们使用的不同工具, 以简化流程。
国际化工具 使PHP软件国际化的最简单方法是使用数组文件。数组将使用转换后的字符串填充, 然后可以从模板中查找它们:
< h1> < ?=$TRANS['title_about_page']?> < /h1>

但是, 对于严肃的项目, 这几乎不是推荐的方法, 因为它肯定会带来将来的维护问题。一些问题甚至可能在一开始就出现, 例如缺乏对变量插值的支持或名词的复数等等。
最经典的工具之一(通常用作i18n和l10n的参考)是一个称为Gettext的Unix工具。
尽管可以追溯到1995年, 但它仍然是翻译软件的综合工具, 该工具也易于使用。虽然很容易上手, 但它仍然具有强大的支持工具。
Gettext是我们在本文中将使用的。我们将展示一个出色的GUI应用程序, 该应用程序可用于轻松更新l10n源文件, 从而避免了需要处理命令行的情况。
图书馆让事情变得轻松
如何构建多语言应用程序(PHP和Gettext演示)

文章图片
有主要的PHP Web框架和库支持Gettext和i18n的其他实现。其中一些文件比其他文件更易于安装, 或者具有其他功能或支持不同的i18n文件格式。尽管在本文档中, 我们着重介绍PHP核心随附的工具, 但以下是一些值得一提的列表:
  • oscarotero / Gettext:具有面向对象接口的Gettext支持;包括改进的帮助程序功能, 功能强大的提取器(用于多种文件格式)(其中一些不受gettext命令本机支持)。还可以导出为.mo / .po文件以外的格式, 如果你需要将翻译文件集成到系统的其他部分(例如JavaScript界面??), 则该格式很有用。
  • symfony /翻译:支持许多不同的格式, 但建议使用详细的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文件选择任意根。在其中, 你将拥有一个用于每个所需语言环境的文件夹, 以及一个包含所有PO / MO对的固定” LC_MESSAGES” 文件夹。
如何构建多语言应用程序(PHP和Gettext演示)

文章图片
复数形式
正如我们在导言中所说, 不同的语言可能具有不同的多元化规则。但是, Gettext节省了我们的麻烦。
创建新的.po文件时, 你必须声明该语言的复数规则, 而对复数敏感的翻译作品的每个规则都有不同的形式。
在代码中调用Gettext时, 必须指定一个与句子相关的数字(例如, 短语” You have n messages” 。你将需要指定n的值), 并且它将得出正确的格式使用-甚至在需要时使用字符串替换。
多个规则由每个规则的布尔测试所需的规则数量组成(最多可以省略一个规则的测试)。例如:
  • 日语:nplurals = 1;复数= 0; -一个规则:没有复数形式
  • 英语:nplurals = 2;复数=(n!= 1); -两个规则:仅当n不为1时才使用复数形式, 否则使用单数形式。
  • 巴西葡萄牙语:nplurals = 2;复数=(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 "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"

第一部分的工作方式类似于标头, 其中的msgid和msgstr为空。
它描述了文件编码, 复数形式以及其他一些内容。第二部分将简单的字符串从英语翻译为巴西葡萄牙语, 第三部分进行相同的操作, 但是利用了sprintf的字符串替换, 从而使翻译能够包含用户名和访问日期。
最后一部分是复数形式的示例, 将单数和复数形式显示为英语中的msgid, 并将其对应的翻译显示为msgstr 0和1(遵循复数规则给出的数字)。
在那里也使用了字符串替换, 因此可以使用%d在句子中直接看到数字。复数形式始终具有两个msgid(单数和复数), 因此建议不要使用复杂的语言作为翻译来源。
本地化键
你可能已经注意到, 我们使用实际的英语句子作为来源ID。该msgid与所有.po文件中使用的msgid相同, 这意味着其他语言将具有相同的格式和相同的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 id="header"> < 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.准备第一轮翻译 与自定义框架i18n软件包相比, Gettext具有的巨大优势之一是其广泛而强大的文件格式。
也许你是在想:” 哦, 伙计, 很难手工理解和编辑, 简单的数组会更容易!” 没错, Poedit之类的应用程序可以为你提供很多帮助。你可以从他们的网站上获得该程序, 该程序是免费的, 并且适用于所有平台。使用Gettext提供的所有功能, 这是一种非常容易习惯的工具, 同时又是一个非常强大的工具。我们将在这里使用最新版本Poedit 1.8。
如何构建多语言应用程序(PHP和Gettext演示)

文章图片
在第一次运行中, 你应该从菜单中选择” 文件> 新建… ” 。系统会要求你提供语言;选择/过滤你想要翻译的语言, 或使用我们之前提到的格式, 例如en_US或pt_BR。
如何构建多语言应用程序(PHP和Gettext演示)

文章图片
现在, 保存文件-也使用我们提到的目录结构。然后, 你应点击” 从源中提取” , 然后在此处为提取和翻译任务配置各种设置。你稍后可以通过” 目录> 属性” 找到所有这些内容:
源路径:包括项目中调用gettext()(及其兄弟姐妹)的所有文件夹-通常是你的template / views文件夹。这是唯一的强制设置。
翻译属性:
  • 项目名称和版本, 团队和团队的电子邮件地址:.po文件标题中的有用信息。
  • 复数形式:这些是我们前面提到的规则。大多数时候, 你都可以将其保留为默认选项, 因为Poedit已经包含了方便使用的多种语言复数规则数据库。
  • 字符集:最好是UTF-8。
  • 源代码字符集:你的代码库使用的字符集-可能也是UTF-8, 对吧?
源关键字:基础软件知道gettext()和类似的函数调用在几种编程语言中的外观, 但是你也可以创建自己的翻译函数。你将在此处添加其他方法。这将在后面的” 技巧” 部分中讨论。
设置完这些属性后, Poedit将扫描你的源文件以查找所有本地化调用。每次扫描后, Poedit将显示发现的内容和从源文件中删除的内容的摘要。新条目将在转换表中为空, 从而允许你输入这些字符串的本地化版本。保存该文件后, .mo文件将被(重新)编译到同一文件夹中, 并且, 你的项目已经国际化!
如何构建多语言应用程序(PHP和Gettext演示)

文章图片
Poedit还可以建议网络和以前的文件中的常用翻译。这很方便, 因此你只需要检查它们是否有意义并接受即可。如果不确定翻译, 可以将其标记为” 模糊” , 并且将以黄色显示。蓝色条目是没有翻译的条目。
4.翻译字符串 你可能已经注意到, 本地化字符串有两种主要类型:简单的字符串和具有复数形式的字符串。
简单的只有两个框:源字符串和本地化字符串。由于Gettext / Poedit不具有更改源文件的功能, 因此无法修改源字符串。相反, 你将需要更改源本身并重新扫描文件。 (提示:如果右键单击翻译行, 它将显示提示以及源文件和使用该字符串的行。)
复数形式的字符串包括两个框以显示两个源字符串, 以及制表符, 以便你可以配置不同的最终形式。
如何构建多语言应用程序(PHP和Gettext演示)

文章图片
Poedit上具有复数形式的字符串的示例, 其中显示每个字符串的翻译选项卡。
每当你更改源代码文件并需要更新翻译时, 只需单击刷新, Poedit就会重新扫描代码, 删除不存在的条目, 合并已更改的条目并添加新的条目。
Poedit可能还会根据你所做的其他翻译尝试猜测一些翻译。这些猜测和更改的条目将收到一个” 模糊” 标记, 表明它们需要审阅, 并以黄色显示在列表中。
如果你有翻译团队, 并且有人试图写一些他们不确定的东西, 这也很有用:只需将其标记为” 模糊” , 其他人以后会对其进行审查。
最后, 建议你保留” 查看> 未翻译的条目优先” 标记, 因为这将有助于你避免忘记任何条目。在该菜单中, 你还可以打开UI的某些部分, 以便在需要时为翻译人员保留上下文信息。
提示与技巧 Web服务器最终可能会缓存你的.mo文件。
如果你将PHP作为模块在Apache(mod_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之类的工具, 它比以往任何时候都更加容易。
相关:PHP 7简介:新增功能和功能

    推荐阅读