forrest79 / translation
简单快捷的翻译工具,用于国际化您的PHP应用程序。
Requires
- php: ^8.0
Requires (Dev)
- forrest79/phpcs: ^1.5
- forrest79/phpcs-ignores: ^0.4
- latte/latte: ^3.0
- nette/application: ^3.1
- nette/neon: ^3.4
- nette/tester: ^2.5
- phpstan/phpstan: ^1.10
- phpstan/phpstan-strict-rules: ^1.5
- tracy/tracy: ^2.10
Suggests
- neon/neon: To use with Neon catalogue loader.
- nette/application: To use with Nette translator factory and auto detecting current locale.
- tracy/tracy: For logging and debug panel.
README
简单快捷的翻译工具,用于国际化您的PHP应用程序。
要求
Forrest79/Translation 需要 PHP 8.0 或更高版本。
安装
- 使用 Composer 将 Forrest79/Translation 安装到您的项目中
$ composer require forrest79/translation
文档
基础
存在一个主要的 Translator 对象用于翻译消息。每个 Translator 对象对具体区域是不可变的,可以通过 TranslatorFactory 或手动创建。
消息通过 CatalogueLoader 加载。您可以编写自己的或使用提供的来自 neon 文件的加载器。
为了最佳性能,目录被缓存到PHP文件中。您需要负责使缓存无效以强制重新加载目录。Neon加载器在目录neon文件更新时自动在调试模式下使缓存无效。
CatalogueLoader 可以返回区域(如果缺失,翻译器将尝试根据区域/语言代码使用内部定义)的复数定义,并必须返回传递区域的翻译消息结构。
复数部分包含复数的条件。输入是计数,返回是具体复数翻译的零基于索引。它使用 $i 变量操作,例如英语可以这样看:($i === 1) ? 0 : 1。这意味着 - 如果计数是 1,则获取位置 0 的翻译,否则获取位置 1 的翻译。因此,所有英语复数消息都必须包含两个翻译。
复数助手
大多数语言的复数定义在 PluralsHelper 中,它基于 symfony/translation。当目录中缺少复数定义时,内部使用。
为了正确检测区域,使用正确的区域名称 - 2个字符的语言代码(en、de、fr、cs、...)或区域代码(en_US、en_GB、de_DE、fr_FR、cs_CZ、...)。
翻译器
Translator 对象有主要方法 translate(string $message, array $parameters = [], int|NULL $count = NULL)。
唯一必需的参数是 $message。如名称所示,这是从目录中翻译的消息。建议使用标识符(web.form.password)作为消息,而不是真实文本(输入密码)。
您的消息可以包含一些动态参数,在翻译时用实际值替换。将这些参数作为第二个参数 $parameters 传递。例如
$translator->translate('web.error.message', ['max_length' => Validator::MAX_LENGTH]);
消息必须包含 %max_length% 参数 - 它是参数名称,由 % 字符包围。例如 Text must be %max_length% long.。
最后,如果您正在翻译复数消息,请使用第三个参数 $count。与参数结合使用
$translator->translate('web.error.message', ['max_length' => Validator::MAX_LENGTH, 'entered_length' => number_format(strlen($text))], strlen($text));
消息可能如下所示
[
'web.error.message' => [
'Text must be %max_length% long. You enter %entered_length% character.',
'Text must be %max_length% long. You enter %entered_length% characters.',
],
]
或者只是一个简单的复数消息
$translator->translate('web.error.message', count: strlen($text));
消息可能如下所示
[
'web.error.message' => [
'Only one characted was entered.',
'Too many characters was entered.',
],
]
其他方法仅用于设置记录器(setLogger() - 关于此的更多信息稍后提供)、获取当前区域(getLocale())、当前后备区域(getFallbackLocales())和清除区域缓存(cleanCache() - 缓存将在下一次请求时重建)。
要创建一个 Translator 对象,您必须提供
bool $debugMode- 在调试模式下引发关于错误的异常(不是关于缺失的翻译),在生产模式下,这些只是记录下来Catalogues $catalogues对象 - 关于此的更多信息稍后提供string $locale- 区域名称数组 $fallbackLocales- 回退区域名称,按照优先级顺序。当主区域的翻译缺失时(相关信息将被记录),如果第一个回退区域也缺失,则尝试第二个,以此类推...如果找不到翻译,则返回消息标识符作为翻译。
Catalogues 对象是本库的核心。它提供目录加载、缓存、失效缓存以及目录中的消息搜索。
要创建此对象,您必须提供
bool $debugMode- 在调试模式下,目录可以自动重新加载。在生产模式下,您必须手动删除缓存的文件(自动重新加载仅在调试模式下工作,原因是在每个请求上检查是否需要重新加载可能很昂贵)string $tempDir- 缓存的区域将保存在$tempDir/cache/localesCatalogueLoader $catalogueLoader- 某些目录加载器,您的自己的(例如,从数据库加载消息)或内部Neon目录加载器CatalogueUtils $catalogueUtils- 可选,稍后介绍
您可以手动准备这些对象,但首选的方式是使用 TranslatorFactory...
TranslatorFactory
TranslatorFactory 将帮助您创建 Translator 对象。要创建工厂,您必须提供
bool $debugModestring $tempDirCatalogueLoader $catalogueLoaderarray $fallbackLocales- 可选 - 您可以为区域指定其回退区域 - 例如['en' => ['de', 'fr'], 'fr' => ['de'], 'de' => ['en']]- 英语有回退德语和法语,法语有回退德语和德语,没有回退定义的区域没有回退,您不需要在这里使用空数组CatalogueUtils $catalogueUtils- 可选 - 如果未提供且加载了 Opcache 扩展(存在函数opcache_invalidate()),则自动使用CatalogueUtils\OpcacheLogger $logger- 可选
准备好 TranslatorFactory 后,只需调用 create(string $locale, array|NULL $fallbackLocales = NULL) 方法,您将获得 $locale 的 Translator 对象。
如果 $fallbackLocales 是 NULL,则回退区域从 TranslatorFactory 上定义的值获取,如果它是数组(即使是空数组),则使用此值。
相同 $locale 和 $fallbackLocales 的 Translator 对象被缓存。如果您为相同的参数两次调用 create() 方法,您将获得相同的 Translator 对象。
Catalogues
目录中的消息可以是简单的键值对 - message => translation。或者可以包含变量 (%var%) 或复数。
这是 neon 格式的示例
messages: simpleMessage: This is simple message. messageWithVariable: Hello %user%. pluralMessageForEn: - One item. - More items. pluralMessageForEnWithVariable: - I have %count% car for user %user%. - I have %count% cars for user %user%.
目录可以包含复数的信息。这是 neon 格式中 en 区域的示例
plural: '($i === 1) ? 0 : 1'
如果您使用内部 CatalogueLoaders\Neon 目录加载器,必须在构造函数中传递存储 neon 文件的目录。
然后,例如,当您想为 en 区域创建 Translator 时,将使用 en.neon 文件。对于 en_US 区域,将使用 en_US.neon 文件。
如果您想实现自己的 CatalogueLoader,您必须实现接口中的这些方法
isLocaleUpdated(string $locale, string $cacheFile): bool- 如果在调试模式下需要重建缓存,则返回TRUE,否则返回FALSE(《CatalogueLoaders\Neon》返回TRUE如果源 neon 文件已更新)loadData(string $locale): array- 返回包含两个键的数组,plural(可选)定义和messages,其中包含message => translation|list的数组(列表用于复数消息)source(string $locale): string- 返回源标识,neon 文件的文件路径或您想要用于标识区域正确源的内容
CatalogueUtils
CatalogueUtils 对象可以响应两个事件
- 当构建缓存文件时(PHP缓存文件在临时目录中创建) -
afterCacheBuild(string $locale, string $source, string $localeCache)方法 - 当清除缓存时(通过
Translator::clearCache()或Catalogues::clearCache(string $locale)删除PHP缓存文件) -afterCacheClear(string $locale, string $localeCache)方法
默认情况下,使用 TranslatorFactory,当加载Opcache扩展时,使用提供的 CatalogueUtils\Opcache 对象。此对象将清除Opcache中的PHP缓存文件。如果您在生产环境中设置为不自动检查更改的PHP文件(或有一些时间检查),这可能会很棘手。那么在清除或重建缓存之后,您仍然会在Opache重新加载PHP文件内容之前看到旧PHP文件的内容。此 CatalogueUtils 将立即为此文件清除Opcache。
记录器
如果您想在请求期间了解不存在的翻译、错误和加载的区域设置,您可以可选地使用一个 Logger。
Logger 实现了3个方法
addUntranslated(string $locale, string $message)在消息在主区域(不在任何回退区域中)没有翻译时调用addError(string $locale, string $error)- 仅在生产模式下调用(在调试模式下抛出异常)当发生某些错误时,例如您尝试将单数消息翻译为复数或相反addLocaleFile(string $locale, string $source)- 在使用Catalogues对象加载新区域时调用
在此库中提供了两个记录器(您可以编写自己的记录器)
Loggers\TracyLogger- 为生产环境准备,使用Tracy\ILogger记录未翻译的消息和错误Loggers\TracyBarPanel- 为开发环境准备,在Tracy BarPanels中显示未翻译的消息和加载的目录
提取器
要成功翻译您的应用程序,您需要了解所有需要翻译的文本。实现此目标的一种最佳选项是从应用程序源代码中提取所有文本。为此,库提供了 CatalogueExtractor 和 MessageExtractors。
MessageExtractors 从源代码中提取文本,CatalogueExtractor 检查现有翻译并与提取的消息进行比较,告诉您需要从翻译中添加或删除什么。
存在两个现有的 MessageExtractors
Php- 提取对方法->translate()的所有调用,并使用第一个参数作为消息标识符(->translate('identifier', count: 3)将提取identifier作为消息)Latte- 提取对translate过滤器的所有使用,并使用第一个参数作为消息标识符({='identifier'|translate:3}和{var $trans = ('identifier'|translate:['var' => 'test'])}将提取identifier作为消息)
存在一个现有的 CatalogueExtractor - Neon。它可以读取来自区域 neon 文件的现有翻译,并添加新消息或删除旧消息。它还保留 neon 文件中的注释。
要使用(例如)Neon 目录提取器,您必须准备一个简单的PHP脚本。例如,对于cli,它可以看起来像
#!/usr/bin/env php <?php declare(strict_types=1); require __DIR__ . '/../vendor/autoload.php'; $localesDir = __DIR__ . '/../app/locales'; $locales = ['en', 'cs']; $sourceDirectories = [ __DIR__ . '/../app', ]; $latteEngine = new Latte\Engine(); $latteEngine->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension(NULL)); $latteEngine->addExtension(new Nette\Bridges\FormsLatte\FormsExtension()); $messageExtractors = [ new Forrest79\Translation\MessageExtractors\Php(), new Forrest79\Translation\MessageExtractors\Latte($latteEngine), ]; (new Forrest79\Translation\CatalogueExtractors\Neon($localesDir, $locales, $sourceDirectories, $messageExtractors)) ->extract();
目录提取器需要知道
- 要处理的区域有哪些(
$locales) - 源代码文件所在的目录有哪些(
$sourceDirectories) - 使用哪些消息提取器 -
Php没有依赖项,对于Latte,您必须准备Latte\Engine
Neon目录提取器还需要知道一个目录,用于保存源 neon 区域($localesDir)。
不要在
translate方法或 latte 过滤器中使用变量作为消息标识符,因为这无法提取。
$id = 'identifier'; $translator->translate($id)- 消息提取器不知道$id是identifier,因此这个消息被跳过$translator->translate('identifier')- 当你需要使用变量时,你必须手动将消息添加到目录中,并更新提取器以避免删除它们。
要使用自己的目录提取器,你必须扩展CatalogueExtractor。例如,一个简单的工作于数据库的提取器可能如下所示
$locales = ['en', 'cs']; $sourceDirectories = [ __DIR__ . '/../app', ]; $messageExtractors = [ new Forrest79\Translation\MessageExtractors\Php(), ]; (new class($dbConnection, $locales, $sourceDirectories, $messageExtractors) extends Forrest79\Translation\CatalogueExtractor { private DbConnection $dbConnection; public function __construct( DbConnection $dbConnection, array $locales, array $sourceDirectories, array $messageExtractors, ) { parent::__construct($locales, $sourceDirectories, $messageExtractors); $this->dbConnection = $dbConnection; } protected function loadExistingMessages(string $locale): array { return $this->dbConnection->query(' SELECT ti.identifier FROM public.translation_identifiers AS ti LEFT JOIN public.translations AS t ON t.identifier_id = ti.id AND t.lang = ? ', $locale)->fetchPairs(NULL, 'identifier'); } protected function processMessagesToInsert(string $locale, array $messages): void { foreach ($messages as $message) { $this->log(sprintf('SELECT public.translation_insert(constant.lang_%s(), \'%s\', ARRAY[\'\']);', $locale, $message)); } } protected function processMessagesToRemove(string $locale, array $messages): void { foreach ($messages as $message) { $this->log(sprintf('DELETE FROM public.translation_identifiers WHERE identifier = \'%s\';', $message)); } } protected function log(string $message): void { echo $message . PHP_EOL; } }) ->extract();
Nette
这个库可以简单地集成到Nette 框架中。你已经了解两个用于Tracy 调试工具的日志记录器 TracyLogger 和 TracyBarPanel。
还有一个特殊的Nette\TranslatorFactory。这个工厂扩展了经典的TranslatorFactory并添加了一个方法createByRequest(Application\Application $application, string|NULL $defaultLocale = NULL)。
此方法尝试从应用程序请求中检测当前的区域设置。如果没有找到区域设置,则使用$defaultLocale(当$defaultLocale为NULL时,将抛出异常)。
首选的方法是在你的依赖注入容器中注册服务。
services: - Forrest79\Translation\Nette\TranslatorFactory(%debugMode%, %tempDir%, parameter: lang, fallbackLocales: ['en': ['cs'], 'cs': ['en']])::createByRequest() - Forrest79\Translation\CatalogueLoaders\Neon(%appDir%/locales) - Forrest79\Translation\Loggers\TracyLogger
或者在两行中定义工厂
services: - Forrest79\Translation\Nette\TranslatorFactory(%debugMode%, %tempDir%, parameter: lang, fallbackLocales: ['en': ['cs'], 'cs': ['en']]) - @Forrest79\Translation\Nette\TranslatorFactory::createByRequest() - Forrest79\Translation\CatalogueLoaders\Neon(%appDir%/locales) - Forrest79\Translation\Loggers\TracyLogger
这将始终将正确的区域设置设置的Translator对象注册到你的依赖注入容器中。这里的parameter定义了从请求中使用的参数。它可以是某些查询参数或来自路由器的参数。此外,还使用默认的Neon目录加载器。
如果你想覆盖一些服务,例如,在本地环境中使用TracyDebugPanel,请使用服务名称
services: translationLogger: Forrest79\Translation\Loggers\TracyLogger
然后在你的本地配置中覆盖它
services: translationLogger: Forrest79\Translation\Loggers\TracyBarPanel::register()
你可以定义自己的CatalogueUtils
services: - Forrest79\Translation\CatalogueUtils\Opcache
但这个默认会被自动使用,除非你选择另一个。
你可能还想将翻译器注册到Latte中。你可以创建自己的Latte 扩展,带有translate过滤器,或者简单地注册translate过滤器到模板中。过滤器可能如下所示
$template->addFilter('translate', function (string $message, array|int $parameters = [], int|NULL $count = NULL): string { if (is_int($parameters)) { $count = $parameters; $parameters = []; } return $this->translator->translate($message, $parameters, $count); });
在latte中调用
{='identifier'|translate}
{='identifier_with_var'|translate:['var' => 'test']}
{='identifier_plural'|translate:3}
{='identifier_with_var_plural'|translate:['var' => 'test'],3}
或者将输出保存到变量中
{var $trans = ('identifier'|translate)}