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 ofServiceProvider, 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 calledAcme\Courier.
lang/ja/messages.php.
lang/en/messages.php as a fallback.
boot() method.
ja, matching Laravel’s configuration. This is different from the jp used in this documentation site’s URLs.
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 inlang/vendor/courier/ja/messages.php. If the language directory has been changed, use the location under $this->app->langPath('vendor/courier').
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.
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.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.jsonis 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.jsonwithpublishes()does not merge file contents. To avoid breaking existing translations, instruct consumers to add only the keys they need.
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.- 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. - Keep placeholders stable — Renaming
:nameto:recipientalso requires changing the replacement arrays at call sites. Do not treat it as a change to the translation files alone. - Diff the published files — Compare consumers’ overrides with the new defaults. Removing override keys that are no longer needed restores the package’s values.
- Avoid unconditional republishing — Republishing with
--forceoverwrites consumers’ customizations. With a design that copies JSON into the application’s file, other translations may be lost as well. - 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.
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 branch13.x, and the internal implementation against the latest release at the time of reference, v13.35.0.
- Laravel official documentation: Package language files
- Laravel official documentation: Overriding package language files
- ServiceProvider: Registering translations
- TranslationServiceProvider: Default language paths
- FileLoader: Recursive replacement for PHP and JSON load order
- Translator: JSON-first retrieval and loaded arrays
- Official tests: Translation loader