Symfony Translation 组件 CHANGELOG 深度解读:从 2.1 到 8.2 的国际化能力演进与实战指南

原创2026-10-03 09:05:131,639 阅读
文章标签:后端Web框架

Symfony Translation 组件 CHANGELOG 深度解读:从 2.1 到 8.2 的国际化能力演进与实战指南

本篇文章以 src/Symfony/Component/Translation/CHANGELOG.md 为主线,系统梳理 Symfony Translation 组件十余个版本的核心变更,重点解读 8.2 版本中 TranslationBundle 独立成包、翻译命令迁移、PHP 消息提取增强与 XLIFF/PO 格式处理改进,并结合当前仓库源码给出可落地的配置与命令用法。读完你将掌握翻译目录发现机制、lint:translations 与 translation:* 命令族、PHP 源码提取器的能力边界,以及 XLIFF、gettext PO 等格式的加载与导出细节。

一、组件定位与 CHANGELOG 的价值

Translation 组件是 Symfony 生态中负责应用国际化的基础设施,核心类包括 Translator、MessageCatalogue、各类 Loader 与 Dumper。组件的官方定位在 README.md 中只有一句话:为你的应用提供国际化工具,并通过 composer require symfony/translation 安装;其最小用法是配合 ArrayLoader 直接实例化 Translator 并加载内存中的翻译数组。

use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\ArrayLoader;

$translator = new Translator('fr_FR');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', [
    'Hello World!' => 'Bonjour !',
], 'fr_FR');

echo $translator->trans('Hello World!'); // outputs « Bonjour ! »

这份 CHANGELOG 记录从 2.1 到 8.2 的每一处功能增删与破坏性变更,是理解组件能力边界的最佳索引:它既揭示了"现在有什么",也通过 UPGRADE 说明](见仓库根目录 [UPGRADE-8.0.md、UPGRADE-8.1.md、UPGRADE-8.2.md)标明"哪些旧用法已被移除"。下文以 8.2 的最新能力为纲,向前追溯关键演进。

二、8.2:Translation 独立成 Bundle,命令族全面归位

8.2 是组件历史上最重要的一次结构重组,核心动作是把原先由 FrameworkBundle 提供的翻译能力整体下沉到本组件。

2.1 新增 TranslationBundle

CHANGELOG 原文:

  • 新增 Command\TranslationDebugCommand、Command\TranslationExtractCommand 与 CacheWarmer\TranslationsCacheWarmer,这些此前由 FrameworkBundle 提供;
  • 注册全部 translation:* 与 lint:translations 命令,此前由 FrameworkBundle 声明;
  • 新增 TranslationBundle,提供 translation 配置以及原先在 framework.translator 下提供的服务;
  • 新增 DependencyInjection\Translator、TranslationLintCommandPass 与 TranslationUpdateCommandPass,均原属 FrameworkBundle。

在源码层面,TranslationBundle.php 继承了 AbstractBundle,并标注了 #[RequiredBundle(ServicesBundle::class)] 与 #[RequiredBundle(ConsoleBundle::class, ignoreOnInvalid: true)],也就是说它会随 Services 基础设施自动启用,并在控制台存在时挂载命令。其 build() 方法注册了 9 个编译器通道:

$container->addCompilerPass(new RemoveMissingDependenciesPass());
$container->addCompilerPass(new TranslationLintCommandPass(), PassConfig::TYPE_BEFORE_REMOVING, 10);
$container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->addCompilerPass(new DataCollectorTranslatorPass());
$container->addCompilerPass(new LoggingTranslatorPass());
$container->addCompilerPass(new TranslationExtractorPass());
$container->addCompilerPass(new TranslationDumperPass());
$container->addCompilerPass(new TranslatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new TranslatorPathsPass(), PassConfig::TYPE_AFTER_REMOVING);

注意 TranslatorPass 被安排在 TYPE_BEFORE_OPTIMIZATION 阶段且优先级为 -32,代码注释说明这是为了尽量晚地运行,以便拿到 Twig 模板迭代器注册的全部模板路径。这 9 个通道类全部位于 DependencyInjection 目录,其中 TranslationLintCommandPass 与 TranslationUpdateCommandPass 即为 8.2 新增。

configure() 方法完整定义了 translation 配置节点,汇总如下:

配置键 默认值 说明
fallbacks [](未设置时回退到 default_locale) 回退语言列表
logging false 是否启用翻译日志
formatter translator.formatter.default 消息格式化器服务 ID
cache_dir %kernel.cache_dir%/translations 翻译目录缓存位置
default_path %kernel.project_dir%/translations 默认加载翻译文件的目录
paths [] 额外扫描的翻译目录
pseudo_localization 默认关闭 伪本地化(含 accents、expansion_factor、brackets、parse_html、localizable_html_attributes 子项)
providers [] 第三方翻译服务提供者(dsn、domains、locales)
globals [] 全局翻译参数

其中 pseudo_localization.expansion_factor 为不小于 1.0 的浮点数,parse_html 默认 false;providers.locales 未设置时默认使用 framework.enabled_locales 中列出的全部语言。globals 的每个条目必须是字符串,或包含 value/message 键之一的数组,二者不可同时出现也不可同时缺失,否则配置校验会报错(见 configure() 中的 thenInvalid 规则)。

2.2 配置如何驱动容器:目录发现与资源装载

loadExtension() 里有一段值得注意的默认行为:即使 translation 被整体禁用,也会先导入 Resources/config/identity_translator.php,为 Form 和 Validator 组件提供恒等翻译器兜底,保证它们内置翻译不被破坏。启用后则依次导入 translation.php、translation_providers.php,并在存在 Console 组件时导入 console.php,在 kernel.debug 为真时导入 translation_debug.php 并用 translator.data_collector 装饰 translator。

翻译目录的发现由私有方法 discoverTranslationDirs() 完成,它收集三类路径:

  1. 若安装了 symfony/validator、symfony/form、symfony/security-core,通过反射拿到各自包内 Resources/translations 目录(如 Security 的认证异常消息);
  2. 遍历 kernel.bundles_metadata 中每个 bundle 的 Resources/translations 或 translations 目录;
  3. translation.paths 配置项中列出的目录(若不存在会抛出 UnexpectedValueException)。

随后 buildResourceOptions() 用 Finder 扫描这些目录,按 文件名倒数第二段 识别 locale,产出 resource_files、scanned_directories 与 cache_vary 三组容器参数。命名约定是 domain.locale.format,例如 messages.fr.yaml、validators.en.xlf。在 DependencyInjection/Translator.php 的 addResourceFiles() 中可看到同样的解析逻辑:弹出后两段分别作为格式与 locale,其余部分拼成 domain。

2.3 新的依赖注入 Translator 与缓存预热

8.2 新增的 DependencyInjection\Translator 继承了基类 Translator 并实现 WarmableInterface,它持有 cache_dir、debug、resource_files、scanned_directories、cache_vary 五个选项。warmUp() 会跳过未启用缓存的情况,然后对所有启用 locale(未配置时取 fallback locales、当前 locale 与资源 locale 的并集)逐一 loadCatalogue() 预热。

配套的 TranslationsCacheWarmer.php 实现了 CacheWarmerInterface 与 ServiceSubscriberInterface,通过懒加载容器在 warmUp() 时获取 translator 服务,若其实现 WarmableInterface 则委托预热。由于翻译缓存属可选项,isOptional() 返回 true。

doLoadCatalogue() 中还体现了缓存失效的资源追踪:对 vendor 包内的翻译目录使用 composer/installed.json 的 FileResource(包安装时才变化),对应用目录使用 DirectoryResource,对不存在的目录使用 FileExistenceResource——这样任何目录新增文件都能触发容器重建。

三、8.2 的格式与提取能力增强

3.1 保留 XLIFF <source> 元素与 translation:update-xliff-sources

此前加载再导出 XLIFF 时,<source> 内容会被消息键覆盖。8.2 起加载与转储都会保留 <source> 元素(CHANGELOG 原句:Preserve the XLIFF <source> element when loading and dumping a file, instead of overwriting it with the message key)。

同时新增命令 translation:update-xliff-sources,用于用默认语言的译文填充 XLIFF 文件的 <source> 标签。在 TranslationBundle.php 的 loadExtension() 中,该命令的第三个参数被替换为 [...$config['paths'], $config['default_path']],即扫描配置路径与默认路径下的所有 XLIFF 文件。这与 5.1 引入的"xliff2 unit 元素的 name 属性可作翻译键"机制形成互补:name 决定键名,source 保留原文。

3.2 PO 转储器输出 locale 感知的 Plural-Forms 头

PoFileDumper 的导出现在会写入 Plural-Forms 头,并且按 locale 精确匹配(CHANGELOG 原句:Add a locale-aware Plural-Forms header to the output of PoFileDumper)。在 PoFileDumper.php 的 getPluralFormsHeader() 中,规则与 TranslatorTrait::getPluralizationRule() 一一对应:俄语/乌克兰语等输出 nplurals=3,阿拉伯语输出 nplurals=6,日语/韩语/中文等无复数概念的 locale 落到默认 nplurals=1; plural=0;。头部生成还会把 pt_BR、en_US_POSIX 之类的区域变体做归一化处理。这样导出的 .po 文件的 msgstr[n] 索引与头部声明天然一致,可被 gettext 工具链直接消费。

3.3 XLIFF 加载器跳过 vendor 校验

新增 XliffFileLoader 构造参数 $skipValidationDirs,用于对已知合法的目录直接加载、不做 schema 校验(CHANGELOG 原句)。在 XliffFileLoader.php 中,validate() 会先检查资源路径是否命中这些目录前缀,命中则跳过 XliffUtils::validateSchema()。框架集成时传入的是 (new ComposerResource())->getVendors()——因为 vendor 包里的 XLIFF 由各自测试保证,应用侧无法修复,加载时直接跳过校验以提升性能。

3.4 PHP 消息提取支持三元、空合并、match 与插值字符串

8.2 大幅增强了从 PHP 文件提取翻译消息的能力:

  • 支持三元表达式与空合并表达式(? :、??)作为 trans() 的参数;
  • 支持 match 表达式、插值字符串、变量以及方法返回值。

这些能力来自基于 nikic/php-parser 的 PhpAstExtractor(6.2 引入,替代并弃用了旧的 PhpExtractor)。在 PhpAstExtractor.php 中,构造器要求传入一组 AbstractVisitor&NodeVisitor,通过 NodeTraverser 遍历 AST;缺少 nikic/php-parser 时会抛出 LogicException 并提示 composer require nikic/php-parser。实际提取逻辑分布在 Extractor/Visitor 目录下的 TransMethodVisitor(识别 $translator->trans() 调用)、TranslatableMessageVisitor(识别 new TranslatableMessage() 与 t() 辅助函数)、CallResolver(解析方法调用返回值)与 VariableResolver(解析变量与表达式)中。

在 TranslationBundle.php 的 loadExtension() 里可以看到两类提取器的选择逻辑:若 Parser 接口存在(即已安装 php-parser)则移除旧的 translation.extractor.php,否则移除 translation.extractor.php_ast,保证容器只保留可用的一种。

四、翻译命令族:debug、extract、lint 与 provider 推拉

8.2 之前组件在 7.2 引入了 lint:translations,5.3 引入了 translation:pull/translation:push。当前 Command 目录共包含 8 个命令:

命令 引入版本 作用
lint:translations 7.2 校验各 locale 全部 domain 下每条消息能否被正确翻译
translation:debug 8.2(自 FrameworkBundle 迁入) 调试翻译键的覆盖状态
translation:extract 8.2(自 FrameworkBundle 迁入) 从 PHP/Twig 模板提取消息写入目录
translation:pull 5.3 从第三方服务拉取翻译
translation:push 5.3 推送翻译到第三方服务
lint:xliff 3.x 演进 校验 XLIFF 文件语法
translation:update-xliff-sources 8.2 用默认语言译文回填 <source>
(TranslationTrait) — 命令共享的逻辑实现

lint:translations 的实现细节见 TranslationLintCommand.php:它通过 TranslatorInterface&TranslatorBagInterface 获取指定 locale(默认是启用 locale 全集)的目录,遍历每个 domain 的每条消息调用 trans(),捕获 ExceptionInterface;无文件时报错并返回 SUCCESS,存在错误时输出按 locale/domain 分组的错误表并返回 FAILURE,全部通过则输出 "All translations are valid."。命令同时实现了 Console 补全,可对 --locale 选项自动补全启用 locale。

translation:pull 在 6.4 增加了 --as-tree 选项,可将 YAML 消息按树状结构写出。

五、Provider 生态:PoEditor 回归与第三方桥

8.2 重新加入了 PoEditorProvider(CHANGELOG 原句:Re-add PoEditorProvider)。当前仓库的 Bridge 目录包含五个官方提供者:Crowdin、Loco、Lokalise、Phrase 与 PoEditor,每个桥提供独立的 composer 包(如 symfony/po-editor-translation-provider)。

TranslationBundle.php 中的 $classToServices 映射把每个桥的 *ProviderFactory 类与对应服务 ID 关联,只有当 ContainerBuilder::willBeAvailable() 判定对应包未安装时才移除相关服务。而 6.4 新增的 PhraseTranslationProvider 也在这一映射中。Provider 非实验状态在 5.4 起稳定,此前 5.3 的 translation:pull/translation:push 即依赖此机制;4.2 开始使用 ICU parent locales 作为回退语言。

配置一个提供者形如:

translation:
    providers:
        loco:
            dsn: 'loco://API_KEY@default'
            domains: ['messages']
            locales: ['en', 'fr']

locales 不设置时,translation:pull/translation:push 会覆盖 framework.enabled_locales 中的全部语言(对应容器参数 .translator.provider_locales 与 RemoveMissingDependenciesPass 的合并逻辑)。

六、可翻译消息对象与伪本地化(5.x 里程碑)

  • 5.2 引入 TranslatableMessage 对象、t() 全局辅助函数与 PseudoLocalizationTranslator,并支持从 TranslatableMessage 提取消息;t() 定义见 Resources/functions.php。8.0 移除了 TranslatableMessage::__toString(),改为显式调用 trans() 或 getMessage(),7.4 起该 __toString 即已弃用并新增 StaticMessage。
  • 7.3 新增 Translator::addGlobalParameter() 用于定义全局翻译参数,框架层对应 translation.globals 配置;在 TranslationBundle.php 的 loadExtension() 中,全局参数若配置为 value 则直接传入,若配置为 message 则包装成 TranslatableMessage 后再 addGlobalParameter。
  • 6.4 给 LocaleSwitcher::runWithLocale() 的回调传入当前 locale。

伪本地化用于测试界面排版,可在 translation.pseudo_localization 下开启,支持重音替换、括号包裹、扩展系数与 HTML 解析(parse_html 配合 localizable_html_attributes 指定可本地化的属性)。8.1 起 PseudoLocalizationTranslator 在加载 HTML 时强制 LIBXML_NONET 以规避外部实体攻击。

七、关键破坏性变更与升级指引

对升级者最重要的几处:

  • 8.0:CsvFileLoader::setCsvControl() 移除 $escape 参数(7.2 起弃用);DataCollectorTranslator 设为 final(7.1 起);移除 ProviderFactoryTestCase,改用 AbstractProviderFactoryTestCase(7.2 弃用,且 testIncompleteDsnException() 不再默认提供,需自行实现 IncompleteDsnTestTrait);移除 TranslatableMessage::__toString()。
  • 7.0:移除 PhpStringTokenParser 与 PhpExtractor,统一由 PhpAstExtractor 承担提取。
  • 5.0:移除 TranslatorInterface、MessageSelector、ChoiceMessageFormatterInterface、PluralizationRule、Interval 以及 transChoice();复数翻译统一走 trans() 的 %count% 参数;lint:xliff 不再隐式读取 STDIN,需显式追加 -(4.4 起弃用隐式用法)。
  • 4.2:transChoice() 弃用;TranslatorInterface 弃用,改由 Symfony\Contracts\Translation\TranslatorInterface 接替;ICU 消息格式可通过 +intl-icu 后缀的 domain 使用。
  • 2.8:Translator::getMessages() 弃用,改用 TranslatorBagInterface::getCatalogue();FileDumper::format() 弃用,改覆写 formatCatalogue();YamlFileDumper 增加 as_tree、inline 选项;JsonFileDumper 增加 json_encoding 选项——这些选项至今保留,--as-tree 也沿用于 translation:pull。

八、测试与验证

组件配套完整的测试集,位于 Tests 目录,覆盖 92 个 PHP 测试文件以及 27 个 .xlf、12 个 .po、9 个 .yml 等格式样本,可用于验证加载器、转储器、提取器与各命令的行为。阅读测试是理解格式细节(如 XLIFF 1.2/2.0/2.1/2.2、segment-attributes 元数据、复数规则)的捷径:例如 7.2 起 XLIFF 2.0 segment 元素上的属性会以 segment-attributes 元数据返回并由 XliffFileDumper 回写。

结语

从 2.1 的多种格式支持,到 5.x 的 Provider 与可翻译对象,再到 8.2 的独立 Bundle 与命令归位,Symfony Translation 组件的演进始终围绕"提取、管理、渲染、推送"四条主线。升级到 8.2 后,翻译相关命令、配置与缓存预热逻辑全部集中在组件内部,应用层只需关注 translation 配置与翻译文件本身。建议对照 CHANGELOG.md 逐版本核对你的用法,重点检查 5.0 与 8.0 两个破坏性变更节点。

登录后查看全文
symfony