Skip to main content

What this page achieves

You will distribute your package’s messages in multiple languages so that consuming applications can change only the strings they need. The page also covers how to treat translation keys and placeholders as a public API and how to preserve customizations when the package is updated. Localization covers the basics in an application, and Laravel package development covers the basics of registration and publishing. This page digs into the Laravel 13 implementation of ServiceProvider, FileLoader, and Translator.
loadTranslationsFrom() registers where translations are loaded from, while publishes() registers where files are copied to. Consumers do not have to run vendor:publish in order to use the translations.

Distribute PHP translations with a namespace

When you want keys dedicated to your package, use the PHP array format with a namespace. The following example is a package called Acme\Courier.
Provide the Japanese defaults in lang/ja/messages.php.
Also provide English in lang/en/messages.php as a fallback.
Register the loading and the optional publishing in the service provider’s boot() method.
Callers specify the namespace, file name, and array key. The language code is ja, matching Laravel’s configuration. This is different from the jp used in this documentation site’s URLs.
The courier namespace is the second argument to loadTranslationsFrom(). It is not derived automatically from the Composer package name.

PHP translations do not replace the whole file

In the consuming application, with the default language directory, you can write only the keys you want to change in lang/vendor/courier/ja/messages.php. If the language directory has been changed, use the location under $this->app->langPath('vendor/courier').
In this example only queued changes; failed continues to use the package’s Japanese translation.

FileLoader load order

ServiceProvider::loadTranslationsFrom() registers the namespace once the Translator has been resolved. The files themselves are read when a translation is requested. FileLoader::loadNamespaced() reads the registered package’s language file and passes that array to loadNamespaceOverrides(). That method reads vendor/{namespace}/{locale}/{group}.php from each of the loader’s language paths and replaces values using array_replace_recursive(). The default TranslationServiceProvider passes the framework’s language path and the application’s language path to the loader, in that order. Even if an extension registers additional paths, override arrays loaded later take precedence for the same keys.
If the namespace is not registered, FileLoader::loadNamespaced() returns an empty array. Placing files in lang/vendor/courier does not compensate for a missing service provider registration. Also, if another package registers the same namespace, the registered path is replaced, so choose a name that will not collide.

JSON translations have no package-specific namespace

For JSON translations, which use sentences as keys, register a directory as follows. This is an alternative to the PHP translations shown earlier.
An example of the package’s lang/ja.json:
loadJsonTranslationsFrom() has no namespace argument. Registered JSON translations share the same key space as other packages and the application.

JSON overrides go in the application’s ja.json

FileLoader::loadJsonPaths() reads the registered JSON paths first, then the regular language paths, and combines them with array_merge(). In the default setup, the same string key in the application’s lang/ja.json overrides the package’s value.
  • If multiple packages use the same string key, the value from the JSON loaded later wins. Avoid designs that rely on provider order.
  • lang/vendor/courier/ja.json is not an override location for the default JSON loader. Even if you reuse the PHP publishing configuration for JSON, this location is not read automatically.
  • Publishing the package’s JSON to the application’s lang/ja.json with publishes() does not merge file contents. To avoid breaking existing translations, instruct consumers to add only the keys they need.
Translator::get() first checks the JSON for the requested language, and if the key is not found, searches for it as a PHP-style key. Unlike PHP translations, it does not go on to search the fallback language’s JSON. If you use English sentences as JSON keys, distinguish this from the default behavior of displaying the original key when no translation exists.
PHP translation namespaces isolate your PHP keys from those of other packages. However, because Translator::get() checks for exact JSON key matches first, defining a key such as courier::messages.delivery.queued in JSON takes precedence over the PHP side. As a rule, do not mix sentence keys and PHP-style keys.

Update without breaking published translations

For consumers who want to publish all the PHP translations at once, you can provide a targeted command.
However, once all the defaults are copied, the copies themselves become override values from then on. Even if you fix a typo in the package, the new value is not visible as long as the same key remains in the published file. On the other hand, new keys missing from the copy are filled in from the package.
When changing only a few strings, it is easier to pick up updates by placing only the necessary keys in the override file instead of publishing all files. This approach relies on partial overrides of PHP translations.
For long-term maintenance, design updates in the following order.
  1. Keep keys and namespaces stable — Deleting or moving keys affects consumers’ __() calls and override files. Consider adding new keys and keeping the old ones for a transition period.
  2. Keep placeholders stable — Renaming :name to :recipient also requires changing the replacement arrays at call sites. Do not treat it as a change to the translation files alone.
  3. Diff the published files — Compare consumers’ overrides with the new defaults. Removing override keys that are no longer needed restores the package’s values.
  4. Avoid unconditional republishing — Republishing with --force overwrites consumers’ customizations. With a design that copies JSON into the application’s file, other translations may be lost as well.
  5. Verify in long-running processes — Translator::load() keeps arrays per namespace, group, and locale within the instance. In processes where an already-loaded Translator remains, a file change alone does not necessarily trigger a reload. Restart workers and similar processes as appropriate for your deployment.
Choosing what to publish and the overwrite options are covered further in Package public assets and updates, and compatibility decisions when updating versions in Package Version Compatibility Management.

What to verify in a consuming application

In a test application with the service provider registered, verify the following combinations. For setting up a test environment within the package, see Testing Laravel packages with Orchestra Testbench. In tests that create override files after loading, make sure the Translator’s existing loaded results do not affect the outcome. Either prepare the files before retrieving translations, or use a new application instance for each case.

Primary sources

The official documentation was checked against the latest default branch 13.x, and the internal implementation against the latest release at the time of reference, v13.35.0.
Last modified on October 8, 2026