Open-source project
spatie/laravel-translation-loader avatar
spatie/laravel-translation-loader

laravel-translation-loader: Laravel language lines served from the database

Store your translations in the database or other sources

857 stars92 forksPHPMIT

At a glance

What is it?
Spatie's laravel-translation-loader swaps Laravel's translation service provider for one that reads language lines from the database while keeping the __ helper, and it stays useful beyond SQL because any class implementing the TranslationLoader interface can become a translation source.
Who is it for?
laravel-translation-loader fits Laravel and Lumen applications whose translations must change without a deploy, and it fits teams that want a CMS, an admin panel or another system to own the wording of their interface. It is the wrong choice for a project whose language files already ship with the code and never change at runtime, and it adds a database round trip to every miss of the translation cache.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The provider swap is the whole installation story

In a vanilla Laravel or Lumen installation you localise an app with language files. This package enables the translations to be stored in the database instead, and the point of the design is that you keep using all the features of the __ helper you already know:

php
__('messages.welcome', ['name' => 'dayle']);

Nothing in your view layer changes. The same key, the same replacement syntax, the same helper. What changes is where the string comes from.

The mechanism is a service provider replacement. In config/app.php for Laravel or bootstrap/app.php for Lumen, you replace Laravel's own provider:

php
Illuminate\Translation\TranslationServiceProvider::class,

with the one shipped in this package:

php
Spatie\TranslationLoader\TranslationServiceProvider::class,

That single swap is what routes translation loading through the package instead of the framework's file loader. It is also the part to think about hard before installing, because you are removing a framework component from the container and substituting your own, and any other package that expects Laravel's translator internals will meet a different implementation.

The package works on both Laravel and Lumen, with one caveat noted further down.

Database lines win over language files, and that is deliberate

The package does not replace language files, it overlays them. You can mix the two sources, and the resolution rule is explicit: if a translation is present in both a file and the database, the database version will be returned.

That ordering is the feature that makes the package work for applications whose copy changes. A marketing team can edit a string through an admin screen and the change takes effect without a code deploy and without touching the language files on disk. The risk of that same ordering is that a stale database row silently wins over a corrected file, which is worth remembering when you migrate content between environments.

The data model is the LanguageLine model. Creating a line means setting a group, a key and the text per locale:

php
use Spatie\TranslationLoader\LanguageLine;

LanguageLine::create([
   'group' => 'validation',
   'key' => 'required',
   'text' => ['en' => 'This is a required field', 'nl' => 'Dit is een verplicht veld'],
]);

Reading uses the ordinary helper, and the locale switch is the standard framework call:

php
__('validation.required'); // returns 'This is a required field'

app()->setLocale('nl');

__('validation.required'); // returns 'Dit is een verplicht veld'

JSON translation lines are handled by creating a normal LanguageLine with group => '*', which is how you override a JSON string stored under the wildcard group.

The language_lines table comes from published migrations

The storage side is a table named language_lines, and it is not created automatically. You publish the migrations and run them:

bash
php artisan vendor:publish --provider="Spatie\TranslationLoader\TranslationServiceProvider" --tag="translation-loader-migrations"
php artisan migrate

The config file is optional and published the same way:

bash
php artisan vendor:publish --provider="Spatie\TranslationLoader\TranslationServiceProvider" --tag="translation-loader-config"

The published config file has three keys, and each is a deliberate extension point. The translation_loaders key holds the list of loaders that fetch translations, the model key names the model used by the Db loader, and the translation_manager key names the manager that overrides the default Laravel translation.loader:

php
    'translation_loaders' => [
        Spatie\TranslationLoader\TranslationLoaders\Db::class,
    ],

The model entry accepts any model that extends Spatie\TranslationLoader\LanguageLine, so you can add your own columns, for example a translation updated timestamp, and have the loader use it. The translation_manager entry is where you replace the object that Laravel's translator asks for groups and lines, which is the hook a team would use to change caching or add query scoping.

The one rough edge is Lumen. The README notes that publishing assets does not work out of the box in Lumen, so you have to copy the files from the repository instead. That is a small manual step, and it is the kind of detail that shows up only after installation.

Any class implementing TranslationLoader can become the source

The database is the default, not the requirement. The package ships with a translation provider that fetches translations from the database, and if your translations live in a yaml file, a csv file or somewhere else, you extend the package by writing your own provider.

A translation provider is any class implementing the Spatie\TranslationLoader\TranslationLoaders\TranslationLoader interface, and that interface contains exactly one method:

php
namespace Spatie\TranslationLoader\TranslationLoaders;

interface TranslationLoader
{
    /*
     * Returns all translations for the given locale and group.
     */
    public function loadTranslations(string $locale, string $group): array;
}

Registration happens in the translation_loaders key of the config file. The contract is narrow: given a locale and a group, return an array of translations. A YAML or CSV provider is a file read and an array build. A provider that calls an internal translation management API is a network call per group per locale, which is where the caching behaviour of the translation manager starts to matter.

Because loaders are a list rather than a single value, you can register more than one. The README does not spell out the precedence between multiple registered loaders, so if you plan to combine sources you should verify the ordering yourself rather than assume the database always wins across loaders.

Where this is the wrong tool

The first case is an application whose copy never changes at runtime. If your language files ship with the code and a deploy is how text changes, the database layer adds a query and an operational surface for no benefit.

The second is the runtime cost of a cold cache. The framework's file loader reads from disk; this one reads rows. Once the translator has cached a group, the cost is low, but the first request for each locale and group in a request cycle pays for it, and a deployment that swaps a provider also invalidates whatever the translator had cached.

The third is the provider swap itself. You are replacing a framework service provider with your own, which means you own the behaviour the framework's translator provided, including edge cases around fallback locales and JSON groups. The README documents the loader interface and the config keys, and it does not document how the package interacts with every framework feature that touches the translator.

The fourth is the Lumen caveat, already noted: publishing assets does not work out of the box, so a Lumen deployment has manual steps in its install path.

The fifth is version discipline. The package follows semantic versioning with 2.x releases, and the recent ones are 2.8.1 on 2024-12-09, 2.8.2 on 2025-02-21 and 2.8.3 on 2026-02-21, with the last push on 2026-06-02. Reading the CHANGELOG before upgrading is the documented way to see what changed.

Licence, project layout and the upgrade path

The package is released under the MIT License, with the terms in the LICENSE.md file at the repository root and a licence badge in the README. MIT is permissive, so using it inside a commercial Laravel application does not oblige you to publish your own code.

The repository layout is small and conventional for a Spatie package. The top level holds .editorconfig, .gitattributes, .github/, .gitignore, .php-cs-fixer.php, CHANGELOG.md, LICENSE.md, README.md, composer.json, config/, database/, phpunit.xml, src/ and tests/. So the package code sits in src/, the migrations in database/, the config in config/, and the test suite in tests/ with a phpunit.xml at the root. Code style is enforced by php-cs-fixer through the .php-cs-fixer.php configuration.

Installation is a single Composer requirement:

bash
composer require spatie/laravel-translation-loader

and the test suite runs with:

bash
composer test

For bug reports on behaviour, the project asks you to mail [email protected] rather than use the issue tracker when the issue concerns security, and points contributors at a shared CONTRIBUTING guide for other work. The homepage for the package is a blog post by Freek Van der Herten on freek.dev, which is the narrative explanation of why the package exists.

Spatie is a web design agency in Antwerp, and this package sits in a wider set of open source projects; the package itself is maintained on its own cadence, which is what the release list and the last push date tell you.

Editorial conclusion

laravel-translation-loader fits Laravel and Lumen applications whose translations must change without a deploy, and it fits teams that want a CMS, an admin panel or another system to own the wording of their interface. It is the wrong choice for a project whose language files already ship with the code and never change at runtime, and it adds a database round trip to every miss of the translation cache. Before adopting it, confirm that your deployment can run the migrations and that the provider swap in config/app.php or bootstrap/app.php is acceptable, then check whether you need the TranslationLoader interface rather than the built-in Db loader, since the release history runs 2.8.2 on 2025-02-21 and 2.8.3 on 2026-02-21 with a last push on 2026-06-02.

Frequently asked questions

How do I install spatie/laravel-translation-loader?

Run composer require spatie/laravel-translation-loader, replace Laravel's Illuminate\Translation\TranslationServiceProvider with Spatie\TranslationLoader\TranslationServiceProvider in config/app.php or bootstrap/app.php, then publish the migrations with php artisan vendor:publish --provider="Spatie\TranslationLoader\TranslationServiceProvider" --tag="translation-loader-migrations" and run php artisan migrate.

Can I keep using language files together with database translations?

Yes. The README states you can mix language files and the database, and that when a translation is present in both a file and the database, the database version is returned. JSON lines can be overridden by creating a LanguageLine with group => '*'.

Can laravel-translation-loader load translations from something other than the database?

Yes. A translation provider is any class implementing the Spatie\TranslationLoader\TranslationLoaders\TranslationLoader interface, whose single method is loadTranslations(string $locale, string $group): array. Providers are registered in the translation_loaders key of the config file.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. spatie/laravel-translation-loader on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spatie-laravel-translation-loader.svg)](https://hysenlabs.com/projects/spatie-laravel-translation-loader)

Was this useful?