Model or dataset
spatie/laravel-translatable avatar
spatie/laravel-translatable

One JSON column, four locale queries and one unfinished example: inside spatie/laravel-translatable

Making Eloquent models translatable

2,462 stars295 forksPHPMIT

At a glance

What is it?
The package keeps every locale of a translatable Eloquent column inside that column as JSON, then adds four scoped query methods on top. The readme walks the happy path in full and stops mid-sentence inside the nested key feature.
Who is it for?
This suits a Laravel codebase that wants translated fields without owning a translations table, and it buys that by giving up a per language schema and any query you can express in SQL against separate columns. Before committing, read the documentation site for fallback behaviour and column casting rules, since the readme states neither, and check that reading an attribute through one application wide locale matches how your views and API responses pick a language.
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 9 days 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 November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • spatie/laravel-translatable2,462 ★ · +1
Star history
Repository2026-09-302026-11-14
spatie/laravel-translatable2.5k2.5k

1 Stars in the last 45 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Translations live in the column you already have

The design decision is the whole package. Translations are stored as json inside the column itself, so no extra table is needed to hold them. You attach one trait to a model and name the columns that carry translations. The current form is a PHP attribute that accepts a variadic list of column names, so the number of translatable columns is not fixed by the package.

php
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\Attributes\Translatable;
use Spatie\Translatable\HasTranslations;

#[Translatable('name', 'description')]
class NewsItem extends Model
{
    use HasTranslations;

    // ...
}

Every sample class extends the framework's Eloquent model and imports the same namespace, so this is an addition to a model you already have. Nothing in the sample creates a migration, a second model, or a join table.

Two ways to declare columns, and neither one wins

A second form is also accepted: a public property on the model. The two are not alternatives where one overrides the other. When both the property and the attribute are present, their values are merged and deduplicated.

php
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;

class NewsItem extends Model
{
    use HasTranslations;

    public $translatable = ['name'];
}

The consequence is mechanical but easy to trip over: since neither list replaces the other, a column keeps being treated as translatable until it is removed from both places. Deduplication means a column named in both declarations is registered once rather than twice. The two samples above never overlap, the property holds a single name while the attribute holds two, so nothing here shows what a duplicate looks like in practice.

The attribute accessor follows the application locale

Writes go through setTranslation, and reads come in two shapes: an attribute access that follows the application locale, and an explicit call that names the locale.

php
$newsItem = new NewsItem;
$newsItem
   ->setTranslation('name', 'en', 'Name in English')
   ->setTranslation('name', 'nl', 'Naam in het Nederlands')
   ->save();

$newsItem->name; // Returns 'Name in English' given that the current app locale is 'en'
$newsItem->getTranslation('name', 'nl'); // returns 'Naam in het Nederlands'

app()->setLocale('nl');
$newsItem->name; // Returns 'Naam in het Nederlands'

$newsItem->getTranslations('name'); // returns an array of all name translations

The accessor takes no locale argument. Switching the container locale changes what the same property returns on the same object, so the current language belongs to the application rather than to the record. Every sample here reads a locale that has an entry stored for it, and nothing in the file states what the accessor yields when the current locale has no value, or which locale is consulted next.

Four scoped queries, and the plural is spelled Locales

Retrieval comes in four scoped query methods whose names do not describe the queries they build.

php
// Returns all news items with a name in English
NewsItem::whereLocale('name', 'en')->get();

// Returns all news items with a name in English or Dutch
NewsItem::whereLocales('name', ['en', 'nl'])->get();

// Returns all news items that has name in English with value `Name in English`
NewsItem::query()->whereJsonContainsLocale('name', 'en', 'Name in English')->get();

// Returns all news items that has name in English or Dutch with value `Name in English`
NewsItem::query()->whereJsonContainsLocales('name', ['en', 'nl'], 'Name in English')->get();

// The last argument is the "operand"

The pair without json in the name asks only which locales exist for a column. The pair with json in the name also matches a value, and that last argument is called the operand. There is no whereLocaleIn to learn, the plural forms are simply Locales. In the bilingual example both locales share one operand value, so the value is not matched per locale and a Dutch entry holding that same English string would satisfy the query too.

The nested key feature stops one comment early

One feature is announced and then not shown. The final code block in the readme says nested keys of a JSON column can be translated with the arrow notation, then tells the reader to add the path to a variable named $t, and the file ends on that line.

php
// You can translate nested keys of a JSON column using the -> notation
// First, add the path to the $t

No line assigns $t anywhere in the file, and the second sentence is cut off before it names the method that consumes the path. Arrow notation is therefore the one thing here a reader cannot paste into a model, while everything ahead of it can be copied as printed. If nested keys matter to your schema, that gap is the part to resolve elsewhere before writing any model code.

The only shell command in the readme is composer test

Setup is nearly absent. The single bash block in the file is:

bash
composer test

There is no composer require line, no stated PHP version, and no note about whether the target column needs a json cast for the accessor and the scoped queries to work. Two badges point at the package page on Packagist, and the documentation link sends readers to spatie.be/docs/laravel-translatable. The repository root does carry phpstan.neon.dist and phpunit.xml.dist, so static analysis and the test runner are configured for contributors, along with a resources directory that the readme never mentions.

Two tags under ten minutes apart, then two months of commits

The tags read as a feature release and its immediate patch. 6.13.0 landed on 21 February 2026, then 6.14.0 and 6.14.1 were both published on 23 April 2026 under ten minutes apart. The repository is not archived, and the most recent commit is dated 26 June 2026, which leaves roughly two months of work on the default branch with no tag above 6.14.1. CHANGELOG.md sits at the root beside the license file. If you pin a patch level, read that changelog rather than assume 6.14.1 carries everything committed since 6.14.0.

MIT terms, postcard requests and a borrowed idea

The license is MIT, and the readme also asks for postcards. A Support us section links to buying a paid product, and a Postcardware section prints a street address, Spatie, Kruikstraat 22, 2018 Antwerp, Belgium, asking anyone who ships the package to production for a card, with received postcards published on a company page. None of that is a license condition. Credit goes to Freek Van der Herten and Sebastian De Deyne, and the idea of storing translations as json in a column is credited to Mohamed Said, with parts of the readme taken from his multilingual package. The Alternatives section names exactly one project, DB-Fields-Translations, and that name alone frames the trade: fields per language in the database instead of one json column per translatable attribute.

Editorial conclusion

This suits a Laravel codebase that wants translated fields without owning a translations table, and it buys that by giving up a per language schema and any query you can express in SQL against separate columns. Before committing, read the documentation site for fallback behaviour and column casting rules, since the readme states neither, and check that reading an attribute through one application wide locale matches how your views and API responses pick a language.

Frequently asked questions

Does spatie/laravel-translatable need its own translations table?

No. Translations are stored as json inside the column itself, so no extra table is needed to hold them. A model opts in by using the HasTranslations trait and naming its translatable columns.

How does spatie/laravel-translatable decide which columns are translatable?

Two ways, and they combine. The Translatable attribute takes a variadic list of column names, and a public $translatable property does the same. When both are present their values are merged and deduplicated, so a column named twice is registered once.

How do I perform translation in Laravel with spatie/laravel-translatable?

The package assumes an existing Laravel application and does not explain the framework itself. What it shows is setTranslation for writes, getTranslation and getTranslations for reads, and four scoped query methods for filtering records by locale.

What is Laravel and why is it used, according to spatie/laravel-translatable?

The repository does not answer that. It is a package that expects a Laravel application and an Eloquent model to attach to, and every code sample imports Illuminate\Database\Eloquent\Model. Anything about why teams pick the framework belongs to the framework's own documentation.

Does spatie/laravel-translatable document how to install it?

Not in the repository. The only shell command in the readme is composer test, and the badges point at the Packagist package page while the documentation link points at spatie.be/docs/laravel-translatable.

When was spatie/laravel-translatable last updated?

The most recent commit is dated 26 June 2026 and the repository is not archived. The newest tag is 6.14.1, published on 23 April 2026, with 6.14.0 released under ten minutes earlier the same day.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. spatie/laravel-translatable 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-translatable.svg)](https://hysenlabs.com/projects/spatie-laravel-translatable)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/spatie-laravel-translatable.svg)](https://hysenlabs.com/projects/spatie-laravel-translatable)

Was this useful?