vertilia / text
通过 gettext 和 PO 格式进行翻译
Requires
- php: >=7.4
- nikic/php-parser: ^5
Requires (Dev)
- phpunit/phpunit: ^9
README
基于 gettext 概念、工具和 PO 文件的翻译库。
描述
此库旨在继续使用 gettext 作为项目国际化方法,保留经过验证的本地化方法,但消除对复杂的目标系统配置和 gettext
php 扩展的依赖。后者可能不适用于特定环境。
标准 gettext 工具链中的 xgettext
提取器不能正确处理所有系统上的 PHP heredoc/nowdoc 语法。我们在这里提供了一个替代的 xtext
脚本,该脚本将字符串提取到 POT 格式,这可以作为您的 gettext 工作流程的源文件使用。
从代码库(通过标准 gettext 方法或使用 xtext
)提取的 PO 目录(代表 gettext 域),然后通过捆绑的 po2php
工具转换为本地的 .php
类。此类在内存中保留翻译,并作为原生 php 代码实现特定语言的规则。
简单消息通过 _()
方法维护,复数形式和上下文通过相应的 nget()
、pget()
和 npget()
方法维护。
优点
从手册中
GNUgettext
被设计为最小化国际化对程序源代码的影响,保持这种影响尽可能小且几乎不可察觉。如果国际化非常轻量级,或者至少在查看程序源代码时看起来如此,那么国际化更有可能成功。
- 无需在源代码中使用常量或其他中间结构替换消息;
- 处理复数形式;
- 处理上下文感知函数(
pgettext
系列),这些函数目前尚未包含在 php 扩展中; - 无需在目标 OS 上安装附加的区域设置或设置环境变量;
- 基于 PO 文件的标准翻译过程;可以使用多种编辑器或过程;
- 翻译存储在
.php
文件中,这允许快速自动加载和 opcache 缓存,最小化将翻译放入内存以及查找源字符串翻译的运行时努力; - 在多进程网络环境中稳定且可预测地工作,不受主机系统配置和当前安装的系统区域设置的限制;
- 捆绑的工具可以正确处理 PHP heredoc/nowdoc 语法,并将 .PO 文件编译为本机 php 代码。
安装
composer require vertilia/text
用法
使用 gettext 在 C 中编程历史上包括以下阶段(简化)
- 定义代码中使用的源消息语言(通常是英语)
- 在处理本地化消息时,在源代码中使用 gettext 函数
- (如果没有现有翻译,gettext 函数简单地返回传递的字符串,因此代码已经在这个阶段工作,在所有环境中返回英语消息)
- 使用 gettext 工具扫描代码并提取本地化消息,生成(或更新)
.po
文本文件- (
.po
文本文件包含从代码中提取的消息,目标语言的翻译以及目标语言复数形式的规则)
- (
- 在
.po
文件中翻译新/更新的消息 - 将文本
.po
文件编译成二进制.mo
文件 - 将
.mo
文件复制到您的代码中,现在 gettext 函数(在第 2 点中提到)可以使用它们来提取其参数的翻译 - 如果在应用程序中添加了新语言,请确保在目标系统上配置了相应的区域设置。
在 Text
中,我们可以跳过第 5、6 和 7 个阶段,并直接将 .po
文件编译成 .php
类,这些类包含目标语言的翻译字符串和复数形式规则。
这些生成的类存储在 locale/
文件夹中,通过 composer 自动加载进行配置。它们就像其他 PHP 代码一样被 opcache 处理,并且在运行时占用最小的空间,因为只需要进行一次 CRC32 变换即可返回现有的翻译(或没有翻译)。
对于大型代码库,就像正常的 gettext 一样,您可以在域上分解翻译,这通常意味着每个域使用不同的 .po
文件。
当您开始项目时,如果您还没有 .po
文件,您可以使用基础 \Vertilia\Text\Text
对象来提供翻译文本。它将简单地返回传递的参数作为翻译消息,这对于调试目的来说通常是足够的。即使在基本形式中,它也已经足够智能,可以正确处理英语消息的复数形式。
<?php include __DIR__ . '/../vendor/autoload.php'; $t = new Vertilia\Text\Text(); echo $t->_('Just a test'), PHP_EOL; // output: Just a test echo $t->pget('page', 'Next'), PHP_EOL; // output: Next echo $t->nget('One page', 'Multiple pages', 1), PHP_EOL; // output: One page echo $t->nget('One page', 'Multiple pages', 5), PHP_EOL; // output: Multiple pages echo $t->npget('page', 'One sent', 'Multiple sent', 5), PHP_EOL; // output: Multiple sent
当您使用 gettext 工具(我们强烈建议使用广泛可用的翻译工具,如 POedit 或其他工具)从上述代码中提取消息时,您将生成一个扩展名为 .po
的文本文件,其中包含源语言消息、目标语言(例如法语)的占位符翻译以及法语复数形式转换的规则。在将 .po
文件中的占位符翻译成法语后,您通常将结果文件包含在您的项目中,作为 locale/fr_FR/LC_MESSAGES/messages.po
。这是一个 GNU 标准,但在 Text
中,您可以使用任何文件夹和文件名。这里最重要的部分是您(以及您的代码库的其他用户)可以轻松地找到翻译文件,并清楚地区分语言和域。
有关在您的代码库上运行 gettext 工具时需要配置的内容,请参阅下文的
xgettext
关键字。
如果
xgettext
在您的系统上无法正常工作,请参阅下文的xtext
参考。
以下是我们项目中 messages.po
文件内容的简化视图
注意您存储的结果 messages.po
文件的位置,因为您将立即需要它来生成翻译类。为此,您将运行捆绑的 po2php
命令(请参阅下文的示例 下面),并给出 messages.po
文件的路径。它将输出您将作为易于定位的 locale-src/MessagesFr.php
文件包含到项目中的 PHP 代码。
vendor/bin/po2php -n App\\L10n -c MessagesFr \ locale/fr_FR/LC_MESSAGES/messages.po \ >src/L10n/MessagesFr.php
因此,目前您已在项目中生成了 2 个额外的文件
locale/fr_FR/LC_MESSAGES/messages.po
:包含来自您的应用程序代码的英语源消息、每条消息的法语翻译以及描述目标语言(法语)中复数形式使用的简单规则的文本文件。您将需要此文件来更新现有翻译、添加新翻译和删除未使用的翻译。这是一个标准的 PO 文件,您可以使用许多可用的工具进行编辑。每个翻译局都会处理此格式(如果它不处理,您最好选择另一个格式)。locale-src/MessagesFr.php
:从messages.po
文件生成的 PHP 类,它封装了目标语言的翻译以及处理法语复数形式的方法。
现在,是时候使用生成的 MessagesFr
类来显示您的翻译消息,而不是使用基础 Text
类了。
<?php require_once __DIR__ . '/../vendor/autoload.php'; $t = new App\L10n\MessagesFr(); echo $t->_('Just a test'), PHP_EOL; // output: Juste un test echo $t->pget('page', 'Next'), PHP_EOL; // output: Suivante echo $t->nget('One page', 'Multiple pages', 1), PHP_EOL; // output: Une page echo $t->nget('One page', 'Multiple pages', 5), PHP_EOL; // output: Plusieurs pages echo $t->npget('page', 'One sent', 'Multiple sent', 5), PHP_EOL; // output: Plusieurs envoyées
有关
composer
的示例配置,请参阅下文的 建议的composer
和git
配置。
现在,您需要为其他语言创建翻译。
流程概述
Vertilia\Text\Text
对象包含处理翻译消息的方法。基类没有翻译,因此其方法简单地返回传递的参数。当将翻译添加到 .po
文件时,它们被保存为扩展 Vertilia\Text\Text
基类并具有翻译和重写 plural()
方法的语言类,以便在目标语言中选择正确的复数形式。
通常,您的代码将包括注入 Vertilia\Text\TextInterface
对象,创建消息,并使用外部 PO 编辑程序从代码中提取消息,处理 .po
文件中的翻译,并使用 po2php
工具更新语言类。
使用 Text
的本地化过程将遵循以下路径
Text
参考
Text::_()
翻译消息
public function _(string $message): string;
参数
$message
源语言中的消息。
返回值
翻译成目标语言的消息(如果没有找到翻译,则为原始消息)。
示例 1:基类,没有翻译
$t = \Vertilia\Text\Text(); echo $t->_("Several words"); // output: Several words
示例 2:翻译 messages.po
为俄语后创建的 MessagesRu
类
$t = \App\L10n\MessagesRu(); echo $t->_("Several words"); // output: Несколько слов
Text::nget()
基于参数的复数形式的翻译。
public function nget(string $singular, string $plural, int $count): string;
参数
$singular
源语言中消息的单数形式。$plural
源语言中消息的复数形式。$count
计数器以选择复数形式。
返回值
根据提供的 $count
在目标语言中翻译消息的复数形式之一。
示例 1:基类,没有翻译
$t = \Vertilia\Text\Text(); printf($t->nget("%u word", "%u words", 1), 1); // output: 1 word printf($t->nget("%u word", "%u words", 2), 2); // output: 2 words printf($t->nget("%u word", "%u words", 5), 5); // output: 5 words
示例 2:翻译 messages.po
为俄语后创建的 MessagesRu
类
$t = \App\L10n\MessagesRu(); printf($t->nget("%u word", "%u words", 1), 1); // output: 1 слово printf($t->nget("%u word", "%u words", 2), 2); // output: 2 слова printf($t->nget("%u word", "%u words", 5), 5); // output: 5 слов
Text::npget()
根据参数翻译消息的复数形式,在给定上下文中。
public function npget(string $context, string $singular, string $plural, int $count): string;
参数
$context
源语言中消息的上下文。$singular
源语言中消息的单数形式。$plural
源语言中消息的复数形式。$count
计数器以选择复数形式。
返回值
根据提供的 $count
在目标语言和上下文中翻译消息的复数形式之一。
示例 1:基类,没有翻译
$t = \Vertilia\Text\Text(); printf($t->npget("star", "%u bright", "%u bright", 1), 1); // output: 1 bright printf($t->npget("star", "%u bright", "%u bright", 2), 2); // output: 2 bright printf($t->npget("star", "%u bright", "%u bright", 5), 5); // output: 5 bright
示例 2:翻译 messages.po
为俄语后创建的 MessagesRu
类
$t = \App\L10n\MessagesRu(); printf($t->npget("star", "%u bright", "%u bright", 1), 1); // output: 1 яркая printf($t->npget("star", "%u bright", "%u bright", 2), 2); // output: 2 яркие printf($t->npget("star", "%u bright", "%u bright", 5), 5); // output: 5 ярких
Text::pget()
在给定上下文中翻译消息。
public function pget(string $context, string $message): string;
参数
$context
源语言中消息的上下文。$message
源语言中的消息。
返回值
目标语言和上下文中的翻译消息。
示例 1:基类,没有翻译
$t = \Vertilia\Text\Text(); printf($t->npget("star", "It's bright")); // output: It's bright
示例 2:翻译 messages.po
为俄语后创建的 MessagesRu
类
$t = \App\L10n\MessagesRu(); printf($t->npget("star", "It's bright")); // output: Она яркая
用于 xgettext
的关键字
为了允许 xgettext
从替换经典 gettext
函数的 Text
方法中提取消息,应提供以下配置给 xgettext
命令行实用程序
xgettext ... --keyword=_ --keyword=pget:1c,2 --keyword=nget:1,2 --keyword=npget:1c,2,3
图形界面工具,如 POedit,将提供配置屏幕,其中可以指定以下列表中的关键字
nget:1,2
pget:1c,2
npget:1c,2,3
为 composer
和 git
提出的配置
在生成 Text
类时,我们建议您将它们存储在应用程序的 L10n/
文件夹中。考虑以下布局(简化,3 种语言)
/app/
├─ locale/
│ ├─ en_US/
│ │ └─ LC_MESSAGES/
│ │ └─ messages.po
│ ├─ fr_FR/
│ │ └─ LC_MESSAGES/
│ │ └─ messages.po
│ ├─ ru_RU/
│ │ └─ LC_MESSAGES/
│ │ └─ messages.po
│ └─ messages.pot
├─ src/
│ ├─ L10n/
│ │ ├─ MessagesEn.php
│ │ ├─ MessagesFr.php
│ │ └─ MessagesRu.php
│ └─ ...
├─ vendor/
│ ├─ autoload.php
│ ├─ composer/
│ └─ vertilia/
├─ www/
│ └─ index.php
├─ .gitattributes
└─ composer.json
在此,您的应用程序代码位于 src/
和 www/
文件夹中,假设应用程序命名空间为 App
,消息类命名空间为 App\L10n
,您的 composer autoload
指令配置如下
{ "autoload": { "psr-4": { "App\\": "src/" } } }
请注意,存储 .po
文件的 locale/
文件夹与 Text
消息类分开存储在 L10n/
文件夹中,以简化从应用程序的二进制版本中排除此文件夹。在生产主机上,您不需要中间文件,因此您很可能将以下行包含到您的 .gitattributes
/locale export-ignore
xtext
参考
vendor/bin/xtext -h
Usage: xtext [OPTIONS] FILES,
OPTIONS:
-h Display usage message and quit
-x EXCLUDE_PATH
Pattern to exclude when scanning FILES, may be repeated to
provide multiple patterns. Executes before -i.
-i INCLUDE_PATH
Pattern to include when scanning FILES, may be repeated to
provide multiple paths. Executes after -x. Default: *
-c COMMENT_TAG
Comment tag to mark extractable comments from the source code.
Use empty string to extract all comments
FILES:
A list of one or more file or directory. If directory names are
specified, they are scanned recursively. Options -x and -i are
used to limit processed files.
EXAMPLES:
xtext *.php
Scan all .php files in current dir
xtext -c '' *.php
Scan all .php files in current dir, also extract comments
xtext -x /*/vendor -x /*/tests -i '*.php' -c TRANSLATORS: /app >msg.pot
Scan all .php files in /app directory and sub-directories,
excluding vendor and tests folders, extract comments starting
with TRANSLATORS: tag and write output to msg.pot file
在某些系统上,用于扫描 PHP 源文件并提取可翻译字符串的 xgettext
工具可能会在 PHP 文件使用 heredoc/nowdoc 语法时中断,特别是使用缩进关闭标识符(自 PHP 7.3 以来可用)。
为了从源代码中正确提取 gettext 行,可以使用捆绑的 xtext
工具。此工具将扫描源文件夹,找到可翻译字符串,并输出包含所有检测到的字符串的 POT 文件。它维护翻译的代码引用,并且还可以包括代码中对相应 Text
和 gettext
函数的调用之前的注释。
上述提到的POedit工具可以使用两种更新现有PO文件的方法,一种是通过使用xgettext
扫描源目录以生成POT文件,并将其透明地合并到现有翻译中,另一种是使用带有提取翻译的现有POT文件。第一种方法更简单,但如果POedit无法自动从您的代码库中的所有文件中提取翻译,则可以使用xtext
生成POT文件,并使用它来更新翻译。
po2php
参考
vendor/bin/po2php --help
Usage: po2php [OPTIONS] messages.po
OPTIONS:
-n, --namespace=NAMESPACE Namespace to use (default: none)
-c, --class=CLASS_NAME Class name (default: Messages)
-e, --extends=PARENT_CLASS Parent class name implementing \Vertilia\Text\TextInterface
(default: \Vertilia\Text\Text)
-5, --php5 Produce php5-compatible code (use php5 branch of Text)
-h, --help Print this screen
示例1:在tests/locale
中生成MessagesRu
目录
vendor/bin/po2php -n App\\Tests\\Locale -c MessagesRu \ tests/locale/ru_RU/LC_MESSAGES/messages.po \ >tests/locale/MessagesRu.php
示例2:使用docker
在tests/locale
中生成MessagesRu
目录
docker run --rm --volume "$PWD":/app php \ /app/vendor/bin/po2php -n App\\Tests\\Locale -c MessagesRu \ /app/tests/locale/ru_RU/LC_MESSAGES/messages.po \ >tests/locale/MessagesRu.php
不同语言中的复数形式
实际上,包含在PO文件中的复数形式选择器是一个C语言代码片段,它返回复数形式的0索引。在大多数情况下,此代码以相当直接的方式翻译成PHP,如下面的示例所示
# English, nplurals = 2
(n != 1)
像英语或德语这样的日耳曼语系语言只有2种复数形式,且不是1的任何数字实际上都是复数
其他语系可能包含更复杂的条件
# Russian, nplurals = 3
(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)
# Russian (alternative form), nplurals = 3
(n%10==0 || n%10>4 || (n%100>=11 && n%100<=14) ? 2 : n%10 != 1)
斯拉夫语系语言如俄语或塞尔维亚语有3种复数形式,用一句话很难描述。单数形式是任何以1结尾(但不是11)的数字。第一个复数形式是任何以2、3或4结尾(但不是12、13或14)的数字。其余所有(包括0、11、12、13和14)都是第二个复数形式。示例
您可以在gettext手册中找到更多示例。
特定语言的复数形式规则重写(在php 8.0之前)
在PHP 8.0之前,三元条件语句在PHP中的结合性不同于C,因此对于使用链式三元运算符作为复数形式选择器的语言,PO文件中的默认规则需要通过在PO文件中使用额外的括号进行更正
# Russian (php7-compat), nplurals = 3
(n%10==1 && n%100!=11 ? 0 : (n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2))
# Russian (php7-compat alternative form), nplurals = 3
(n%10==0 || n%10>4 || (n%100>=11 && n%100<=14) ? 2 : (n%10 != 1))
使用printf()
的复数形式使用
简要来说:要根据变量的值生成使用复数形式的行,请使用以下结构
printf($t->nget("%d file removed", "%d files removed", $n), $n);
在此,对nget
的第一个调用使用$n
将选择对应于单数或复数形式的格式字符串,然后此选定的字符串将被传递给使用另一个$n
的printf
,现在这个$n
将插入相应的%d
占位符。
有关详细讨论,请参阅gettext手册。
gettext函数的类方法替换
注意缺少域函数(dgettext
...)。gettext中的域由不同的翻译文件表示,因此要使用另一个域的翻译,应实例化另一个Text
对象。
示例
gettext
样式putenv('LC_ALL=fr_FR'); setlocale(LC_ALL, 'fr_FR'); bindtextdomain("myPHPApp", "./locale"); textdomain("myPHPApp"); echo gettext("Welcome to My PHP Application"), "\n"; echo dgettext("anotherPHPApp", "Welcome to Another PHP Application"), "\n";
Text
样式$myDomain = \App\L10n\MyPhpAppFr(); $anotherDomain = \App\L10n\AnotherPhpAppFr(); echo $myDomain->_("Welcome to My PHP Application"), "\n"; echo $anotherDomain->_("Welcome to Another PHP Application"), "\n";
此外,虽然上下文感知函数(pgettext
系列)在捆绑的PHP gettext扩展中缺失,但xtext
将在可能的情况下从类似命名的函数中提取字符串。