forrest79/translation

简单快捷的翻译工具,用于国际化您的PHP应用程序。

v2.0.1 2024-04-26 20:56 UTC

README

Latest Stable Version Monthly Downloads License Build codecov

简单快捷的翻译工具,用于国际化您的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个字符的语言代码(endefrcs、...)或区域代码(en_USen_GBde_DEfr_FRcs_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) 方法,您将获得 $localeTranslator 对象。

如果 $fallbackLocalesNULL,则回退区域从 TranslatorFactory 上定义的值获取,如果它是数组(即使是空数组),则使用此值。

相同 $locale$fallbackLocalesTranslator 对象被缓存。如果您为相同的参数两次调用 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中显示未翻译的消息和加载的目录

提取器

要成功翻译您的应用程序,您需要了解所有需要翻译的文本。实现此目标的一种最佳选项是从应用程序源代码中提取所有文本。为此,库提供了 CatalogueExtractorMessageExtractors

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) - 消息提取器不知道$ididentifier,因此这个消息被跳过
  • $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 调试工具的日志记录器 TracyLoggerTracyBarPanel

还有一个特殊的Nette\TranslatorFactory。这个工厂扩展了经典的TranslatorFactory并添加了一个方法createByRequest(Application\Application $application, string|NULL $defaultLocale = NULL)

此方法尝试从应用程序请求中检测当前的区域设置。如果没有找到区域设置,则使用$defaultLocale(当$defaultLocaleNULL时,将抛出异常)。

首选的方法是在你的依赖注入容器中注册服务。

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)}