Skip to main content

本页要实现的目标

以多语言分发包的消息,并让使用方应用程序只修改所需的文案。将翻译键和占位符视为公开 API,并梳理在包更新时保留自定义内容的方法。 本地化介绍应用程序中的基本操作,Laravel 包开发介绍注册与发布的基础。本页将深入 Laravel 13 中 ServiceProvider、FileLoader、Translator 的实现。
loadTranslationsFrom() 注册的是加载位置,publishes() 注册的是文件的复制目标。使用方无需为了使用翻译而必须执行 vendor:publish。

以命名空间分发 PHP 翻译

如果希望拥有包专属的键,请使用 PHP 数组格式和命名空间。以下是名为 Acme\Courier 的包的示例。
在 lang/ja/messages.php 中准备日语的默认值。
在 lang/en/messages.php 中也准备用于回退的英语。
在服务提供者的 boot() 中注册加载以及可选的发布。
使用方需要指定命名空间、文件名和数组键。语言代码按照 Laravel 的设置使用 ja,这与本文档站点 URL 中使用的 jp 不同。
命名空间 courier 是 loadTranslationsFrom() 的第 2 个参数,并不会根据 Composer 的包名自动确定。

PHP 翻译不会整体替换文件

在使用方应用程序中,如果使用标准的语言目录,可以只在 lang/vendor/courier/ja/messages.php 中写入要修改的键。即使更改了语言目录,也应使用 $this->app->langPath('vendor/courier') 下的路径。
在此示例中,只有 queued 发生变化,failed 仍使用包的日语翻译。

FileLoader 的加载顺序

ServiceProvider::loadTranslationsFrom() 在 Translator 解析后注册命名空间。实际的文件读取发生在请求翻译时。 FileLoader::loadNamespaced() 会加载已注册包的语言文件,并将该数组传给 loadNamespaceOverrides()。在那里,它读取加载器各语言路径中的 vendor/{namespace}/{locale}/{group}.php,并通过 array_replace_recursive() 进行替换。 标准的 TranslationServiceProvider 会按此顺序将框架的语言路径和应用程序的语言路径传给加载器。即使存在注册额外路径的扩展,后加载的覆盖数组也会对相同的键优先。
如果命名空间未注册,FileLoader::loadNamespaced() 会返回空数组。仅在 lang/vendor/courier 中放置文件,无法弥补服务提供者的注册遗漏。此外,如果其他包注册了相同的命名空间,注册位置会被替换,因此请选择不会冲突的名称。

JSON 翻译没有包专属的命名空间

对于以句子作为键的 JSON 翻译,按如下方式注册目录。这是与前面的 PHP 翻译不同的另一种选择。
包的 lang/ja.json 示例如下。
loadJsonTranslationsFrom() 没有命名空间参数。已注册的 JSON 翻译与其他包和应用程序共享同一个键空间。

JSON 的覆盖位置是应用程序的 ja.json

FileLoader::loadJsonPaths() 会先读取已注册的 JSON 路径,然后读取常规语言路径,并进行 array_merge()。在标准配置下,应用程序 lang/ja.json 中相同的字符串键会覆盖包的值。
  • 如果多个包使用相同的字符串键,后加载的 JSON 的值优先。应避免依赖 Provider 顺序的设计。
  • lang/vendor/courier/ja.json 不是标准 JSON 加载器的覆盖位置。即使将 PHP 用的发布设置直接套用到 JSON 上,该位置也不会被自动读取。
  • 即使通过 publishes() 将包的 JSON 发布到应用程序的 lang/ja.json,文件内容也不会被合并。为避免破坏现有翻译,请引导使用方只添加所需的键。
Translator::get() 会先检查所请求语言的 JSON,如果找不到,则作为 PHP 格式的键进行查找。它不会像 PHP 翻译那样依次查找回退语言的 JSON。如果将英语句子作为 JSON 键,请将其与“没有翻译时显示原始键”的标准行为区分开来。
PHP 翻译的命名空间可以隔离其他包的 PHP 键。但是,由于 Translator::get() 会先查找 JSON 中完全匹配的键,如果在 JSON 中定义了 courier::messages.delivery.queued 这样的键,它会优先于 PHP 侧。通常应采用不混用句子键和 PHP 格式键的方针。

在不破坏已发布翻译的情况下更新

对于希望一次性发布 PHP 翻译的使用方,可以引导其使用限定对象范围的命令。
但是,如果复制了所有默认值,这些副本此后也都会成为覆盖值。即使在包侧修正了错别字,只要已发布的文件中仍保留相同的键,就看不到新的值。另一方面,副本中没有的新键会由包侧补充。
如果只修改少量文案,不发布全部文件,而只在覆盖文件中放置所需的键,会更容易接收更新。这是利用 PHP 翻译部分覆盖的运维方式。
在长期维护中,请按以下顺序设计更新。
  1. 保持键和命名空间不变 — 删除或移动键会影响使用方的 __() 调用和覆盖位置。请考虑添加新键并保留旧键的过渡期。
  2. 保持占位符不变 — 如果将 :name 改为 :recipient,调用方的替换数组也需要修改。不要将其视为仅修改翻译文件的变更。
  3. 对比检查已发布的文件 — 比较使用方的覆盖与新的默认值。删除不再需要的覆盖键后,即可恢复为包的值。
  4. 避免无条件重新发布 — 使用 --force 重新发布会覆盖使用方的自定义内容。在将 JSON 复制到应用程序文件的设计中,还可能丢失其他翻译。
  5. 在常驻进程中确认 — Translator::load() 会在实例内保存每个命名空间、分组和语言的数组。在仍保留已加载 Translator 的进程中,仅修改文件不一定会重新加载。请根据运维情况重启 Worker 等。
关于发布操作的对象选择和覆盖选项,请参阅包的公开资源与更新;关于版本更新时的兼容性判断,请参阅包的版本兼容性管理。

在使用方应用程序中确认的项目

在注册了服务提供者的验证用应用程序中,确认以下组合。关于包内测试环境的搭建,请参阅使用 Orchestra Testbench 测试 Laravel 包。 在加载后再创建覆盖文件的测试中,要确保 Translator 已有的加载结果不会产生影响。请先准备好文件再获取,或者为每个用例使用新的应用程序实例进行确认。

参考的一手资料

官方文档确认的是最新的默认分支 13.x,内部实现确认的是参考时最新的发布版本 v13.35.0。
最后修改于 2026年10月8日