> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包翻译的覆盖与更新

> 解读 Laravel 13 的翻译加载器，讲解 PHP 翻译的部分覆盖、JSON 翻译的共享键，以及不破坏已发布翻译的更新方法。

## 本页要实现的目标

以多语言分发包的消息，并让使用方应用程序只修改所需的文案。将翻译键和占位符视为公开 API，并梳理在包更新时保留自定义内容的方法。

[本地化](/zh-CN/localization)介绍应用程序中的基本操作，[Laravel 包开发](/zh-CN/advanced/package-development)介绍注册与发布的基础。本页将深入 Laravel 13 中 `ServiceProvider`、`FileLoader`、`Translator` 的实现。

<Info>
  `loadTranslationsFrom()` 注册的是加载位置，`publishes()` 注册的是文件的复制目标。使用方无需为了使用翻译而必须执行 `vendor:publish`。
</Info>

## 以命名空间分发 PHP 翻译

如果希望拥有包专属的键，请使用 PHP 数组格式和命名空间。以下是名为 `Acme\Courier` 的包的示例。

```text theme={null}
courier/
├── src/
│   └── CourierServiceProvider.php
└── lang/
    ├── en/
    │   └── messages.php
    └── ja/
        └── messages.php
```

在 `lang/ja/messages.php` 中准备日语的默认值。

```php theme={null}
<?php

return [
    'delivery' => [
        'queued' => ':nameさんへの配送を受け付けました。',
        'failed' => '配送できませんでした。',
    ],
];
```

在 `lang/en/messages.php` 中也准备用于回退的英语。

```php theme={null}
<?php

return [
    'delivery' => [
        'queued' => 'Delivery to :name has been queued.',
        'failed' => 'Delivery failed.',
    ],
];
```

在服务提供者的 `boot()` 中注册加载以及可选的发布。

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

        $this->publishes([
            __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
        ], 'courier-translations');
    }
}
```

使用方需要指定命名空间、文件名和数组键。语言代码按照 Laravel 的设置使用 `ja`，这与本文档站点 URL 中使用的 `jp` 不同。

```php theme={null}
echo __('courier::messages.delivery.queued', ['name' => '山田'], 'ja');
```

命名空间 `courier` 是 `loadTranslationsFrom()` 的第 2 个参数，并不会根据 Composer 的包名自动确定。

## PHP 翻译不会整体替换文件

在使用方应用程序中，如果使用标准的语言目录，可以只在 `lang/vendor/courier/ja/messages.php` 中写入要修改的键。即使更改了语言目录，也应使用 `$this->app->langPath('vendor/courier')` 下的路径。

```php theme={null}
<?php

return [
    'delivery' => [
        'queued' => ':name様への配送予約が完了しました。',
    ],
];
```

在此示例中，只有 `queued` 发生变化，`failed` 仍使用包的日语翻译。

### FileLoader 的加载顺序

`ServiceProvider::loadTranslationsFrom()` 在 Translator 解析后注册命名空间。实际的文件读取发生在请求翻译时。

`FileLoader::loadNamespaced()` 会加载已注册包的语言文件，并将该数组传给 `loadNamespaceOverrides()`。在那里，它读取加载器各语言路径中的 `vendor/{namespace}/{locale}/{group}.php`，并通过 `array_replace_recursive()` 进行替换。

```mermaid theme={null}
flowchart TD
    A["courier::messages.delivery.queued<br>locale: ja"] --> B["包的<br>lang/ja/messages.php"]
    B --> C["应用程序的<br>lang/vendor/courier/ja/messages.php"]
    C --> D["通过 array_replace_recursive<br>替换指定的键"]
    D --> E["获取翻译字符串并<br>替换占位符"]
```

标准的 `TranslationServiceProvider` 会按此顺序将框架的语言路径和应用程序的语言路径传给加载器。即使存在注册额外路径的扩展，后加载的覆盖数组也会对相同的键优先。

| 状态 | 结果 |
| - | - |
| 应用程序侧存在该键 | 用该值覆盖包的值 |
| 存在相同文件但没有该键 | 保留包中相同语言的值 |
| 在请求的语言中找不到该键 | 通常会查找 `fallback_locale` 的 PHP 翻译 |
| 回退语言中也没有该键 | 标准行为是返回所请求的键 |

<Warning>
  如果命名空间未注册，`FileLoader::loadNamespaced()` 会返回空数组。仅在 `lang/vendor/courier` 中放置文件，无法弥补服务提供者的注册遗漏。此外，如果其他包注册了相同的命名空间，注册位置会被替换，因此请选择不会冲突的名称。
</Warning>

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

对于以句子作为键的 JSON 翻译，按如下方式注册目录。这是与前面的 PHP 翻译不同的另一种选择。

```php theme={null}
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}
```

包的 `lang/ja.json` 示例如下。

```json theme={null}
{
  "Courier delivery queued": "配送を受け付けました。"
}
```

```php theme={null}
echo __('Courier delivery queued', [], 'ja');
```

`loadJsonTranslationsFrom()` 没有命名空间参数。已注册的 JSON 翻译与其他包和应用程序共享同一个键空间。

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

`FileLoader::loadJsonPaths()` 会先读取已注册的 JSON 路径，然后读取常规语言路径，并进行 `array_merge()`。在标准配置下，应用程序 `lang/ja.json` 中相同的字符串键会覆盖包的值。

```json theme={null}
{
  "Courier delivery queued": "配送予約が完了しました。"
}
```

* 如果多个包使用相同的字符串键，后加载的 JSON 的值优先。应避免依赖 Provider 顺序的设计。
* `lang/vendor/courier/ja.json` 不是标准 JSON 加载器的覆盖位置。即使将 PHP 用的发布设置直接套用到 JSON 上，该位置也不会被自动读取。
* 即使通过 `publishes()` 将包的 JSON 发布到应用程序的 `lang/ja.json`，文件内容也不会被合并。为避免破坏现有翻译，请引导使用方只添加所需的键。

<Warning>
  `Translator::get()` 会先检查所请求语言的 JSON，如果找不到，则作为 PHP 格式的键进行查找。它不会像 PHP 翻译那样依次查找回退语言的 JSON。如果将英语句子作为 JSON 键，请将其与“没有翻译时显示原始键”的标准行为区分开来。
</Warning>

PHP 翻译的命名空间可以隔离其他包的 PHP 键。但是，由于 `Translator::get()` 会先查找 JSON 中完全匹配的键，如果在 JSON 中定义了 `courier::messages.delivery.queued` 这样的键，它会优先于 PHP 侧。通常应采用不混用句子键和 PHP 格式键的方针。

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

对于希望一次性发布 PHP 翻译的使用方，可以引导其使用限定对象范围的命令。

```bash theme={null}
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider" --tag=courier-translations
```

但是，如果复制了所有默认值，这些副本此后也都会成为覆盖值。即使在包侧修正了错别字，只要已发布的文件中仍保留相同的键，就看不到新的值。另一方面，副本中没有的新键会由包侧补充。

<Tip>
  如果只修改少量文案，不发布全部文件，而只在覆盖文件中放置所需的键，会更容易接收更新。这是利用 PHP 翻译部分覆盖的运维方式。
</Tip>

在长期维护中，请按以下顺序设计更新。

1. **保持键和命名空间不变** — 删除或移动键会影响使用方的 `__()` 调用和覆盖位置。请考虑添加新键并保留旧键的过渡期。
2. **保持占位符不变** — 如果将 `:name` 改为 `:recipient`，调用方的替换数组也需要修改。不要将其视为仅修改翻译文件的变更。
3. **对比检查已发布的文件** — 比较使用方的覆盖与新的默认值。删除不再需要的覆盖键后，即可恢复为包的值。
4. **避免无条件重新发布** — 使用 `--force` 重新发布会覆盖使用方的自定义内容。在将 JSON 复制到应用程序文件的设计中，还可能丢失其他翻译。
5. **在常驻进程中确认** — `Translator::load()` 会在实例内保存每个命名空间、分组和语言的数组。在仍保留已加载 Translator 的进程中，仅修改文件不一定会重新加载。请根据运维情况重启 Worker 等。

关于发布操作的对象选择和覆盖选项，请参阅[包的公开资源与更新](/zh-CN/advanced/package-assets)；关于版本更新时的兼容性判断，请参阅[包的版本兼容性管理](/zh-CN/advanced/package-versioning)。

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

在注册了服务提供者的验证用应用程序中，确认以下组合。关于包内测试环境的搭建，请参阅[使用 Orchestra Testbench 测试 Laravel 包](/zh-CN/advanced/package-testing)。

| 用例 | 确认内容 |
| - | - |
| 未发布 PHP 翻译 | 能够获取包的日语和英语 |
| 只覆盖日语的 `queued` | `queued` 发生变化，`failed` 保持默认 |
| 包更新时添加了键 | 现有覆盖文件中没有的键也能获取 |
| 请求的语言中没有该键 | PHP 翻译能从设置的回退语言中获取 |
| 在 JSON 中定义相同的键 | 标准配置下应用程序的 JSON 优先 |
| 只在 `lang/vendor/courier` 中放置 JSON | 标准配置下不会成为 JSON 的覆盖 |
| 包含 `:name` 的翻译 | 与调用方的替换数组一致，不残留未替换的字符串 |

在加载后再创建覆盖文件的测试中，要确保 Translator 已有的加载结果不会产生影响。请先准备好文件再获取，或者为每个用例使用新的应用程序实例进行确认。

## 参考的一手资料

官方文档确认的是最新的默认分支 `13.x`，内部实现确认的是参考时最新的发布版本 `v13.35.0`。

* [Laravel 官方文档：包的语言文件](https://github.com/laravel/docs/blob/13.x/packages.md#language-files)
* [Laravel 官方文档：覆盖包翻译](https://github.com/laravel/docs/blob/13.x/localization.md#overriding-package-language-files)
* [ServiceProvider：翻译的注册](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php)
* [TranslationServiceProvider：标准语言路径](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Translation/TranslationServiceProvider.php)
* [FileLoader：PHP 的递归替换与 JSON 的加载顺序](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Translation/FileLoader.php)
* [Translator：JSON 优先的获取与已加载数组](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Translation/Translator.php)
* [官方测试：翻译加载器](https://github.com/laravel/framework/blob/v13.35.0/tests/Translation/TranslationFileLoaderTest.php)


## Related topics

- [Laravel 包开发](/zh-CN/advanced/package-development.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [包视图的覆盖与更新](/zh-CN/advanced/package-views.md)
- [包的公开资源与更新](/zh-CN/advanced/package-assets.md)
- [包迁移的发布与更新](/zh-CN/advanced/package-migrations.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.