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/locales
CatalogueLoader $catalogueLoader
- 某些目录加载器,您的自己的(例如,从数据库加载消息)或内部Neon
目录加载器CatalogueUtils $catalogueUtils
- 可选,稍后介绍
您可以手动准备这些对象,但首选的方式是使用 TranslatorFactory
...
TranslatorFactory
TranslatorFactory
将帮助您创建 Translator
对象。要创建工厂,您必须提供
bool $debugMode
string $tempDir
CatalogueLoader $catalogueLoader
array $fallbackLocales
- 可选 - 您可以为区域指定其回退区域 - 例如['en' => ['de', 'fr'], 'fr' => ['de'], 'de' => ['en']]
- 英语有回退德语和法语,法语有回退德语和德语,没有回退定义的区域没有回退,您不需要在这里使用空数组CatalogueUtils $catalogueUtils
- 可选 - 如果未提供且加载了 Opcache 扩展(存在函数opcache_invalidate()
),则自动使用CatalogueUtils\Opcache
Logger $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)}