forrest79 / simple-translator
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。当目录中缺少复数定义时,内部使用。
为了正确检测区域设置,请使用正确的区域设置名称 - 两位语言代码(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
)作为消息,而不是实际文本(如Enter 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
- 区域名称array $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)
方法,您就会得到 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%.
目录可以包含关于复数的信息。这是 en
区域的 neon
格式的示例
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<translation>
,列表用于复数消息)source(string $locale): string
- 返回源标识,用于neon文件的文件路径或用于标识特定区域设置的任何内容
目录工具
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文件(或需要花费大量时间检查),那么在缓存清除或重建后,您仍然会看到旧的PHP文件内容,直到Opache重新加载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框架中。你已经了解两个日志记录器TracyLogger
和TracyBarPanel
,它们是Tracy调试工具的一部分。
还有一个特殊的Nette\TranslatorFactory
。这个工厂扩展了经典的TranslatorFactory
,并添加了一个方法createByRequest(Application\Application $application, string|NULL $defaultLocale = NULL)
。
这个方法尝试从应用程序请求中检测当前的区域设置。如果没有找到区域设置,则使用$defaultLocale
(当$defaultLocale
为NULL
时,会抛出异常)。
最佳方式是在你的DI容器中注册服务
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
对象到你的DI容器中。参数定义了从请求中使用的参数。这可能是一些查询参数,或者是从路由器来的参数。还使用了默认的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)}