Symfony Translation 组件 CHANGELOG 深度解读:从 2.1 到 8.2 的国际化能力演进与实战指南
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() 完成,它收集三类路径:
- 若安装了
symfony/validator、symfony/form、symfony/security-core,通过反射拿到各自包内Resources/translations目录(如Security的认证异常消息); - 遍历
kernel.bundles_metadata中每个 bundle 的Resources/translations或translations目录; 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 两个破坏性变更节点。