Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Translatables Laravel Package

organi/translatables

Organi Translatables brings multilingual attribute support to Laravel Eloquent models. Attach a simple trait to store and retrieve translated values per locale, with locale fallbacks and straightforward setup for building multi-language applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Verify before you build. This package has no stars and no dependents, so treat the README as the only contract and read the source before committing to it. Start here, in this order:

  1. composer.json — check the illuminate/* constraint against your Laravel version. Small packages like this often pin only one or two major versions.
  2. The service provider — note whether config and migrations are publishable, and whether the package registers a facade or helper.
  3. config/translatables.php (after publishing) — locales list, fallback locale, storage strategy (JSON column vs. separate translations table), and any cache settings.
  4. The trait itself — the public method names (getTranslation, setTranslation, translate, or similar) and which Eloquent events it hooks.

Minimal setup:

composer require organi/translatables
php artisan vendor:publish --provider="Organi\Translatables\TranslatablesServiceProvider"
php artisan migrate
use Organi\Translatables\Traits\Translatable;

class Product extends Model
{
    use Translatable;

    protected $translatable = ['name', 'description', 'slug'];
}

First use case: a two-locale catalog (e.g. en/nl) where editors fill in per-locale titles and body copy on the same form. Get one model working end-to-end — migration, admin form, front-end render with fallback — before touching a second model. The value of the package is that you don't hand-roll name_en/name_nl columns; confirm that payoff on one model first.

Implementation Patterns

Storage strategy drives everything. Determine early whether the package writes to a *_translations side table or to JSON columns on the parent row. This decides your indexing, your query builder usage, and whether whereHas vs. JSON path expressions are available. If it's a side table, you get real indexes and unique constraints per locale; if it's JSON, you get simpler writes but weaker query options.

Model + form pattern. The trait usually exposes translations as a locale-keyed array. Bind that shape directly to your admin form so a single PUT updates all locales:

// Controller
$product->update($request->validated());

// Form payload
[
    'translations' => [
        'en' => ['name' => 'Widget', 'description' => '...'],
        'nl' => ['name' => 'Artikel', 'description' => '...'],
    ],
]

Validate per locale with wildcards rather than duplicating rules:

'translations.*.name' => ['required', 'string', 'max:255'],
'translations.*.slug' => ['required', 'string', 'distinct'],

Note distinct on slugs — uniqueness across locales, not just within one, is a common requirement you'll have to enforce yourself.

Always eager load. Whatever the relation is called (translations, translationsRelation, etc.), add it to a base query scope or $with on read-heavy models. A listing page of 50 products without eager loading is 50 extra queries, and translation tables are exactly where this creeps in unnoticed.

Fallback handling. Decide whether the package's fallback (usually config('app.fallback_locale')) is enough or whether you need per-field fallback. Marketing copy often has description in one locale only, while name should always exist. Write a small accessor wrapper so the rule lives in one place:

public function displayName(?string $locale = null): string
{
    return $this->getTranslation('name', $locale ?? app()->getLocale())
        ?: $this->getTranslation('name', config('app.fallback_locale'))
        ?: '';
}

Querying translated content. For search, build a scope that constrains to the current locale and falls back when empty. If it's a side table:

$query->whereHas('translations', fn ($q) => $q
    ->where('locale', app()->getLocale())
    ->where('name', 'like', "%{$term}%"));

If it's JSON, you'll need the driver-specific JSON path operators — test on the same DB engine as production (SQLite in tests will not behave like MySQL or Postgres here).

API resources. Keep translation exposure explicit. Returning the whole translations array leaks unpublished locales and bloats payloads. Map to the requested locale in the resource, and accept ?locale= with validation against the configured list.

Import/export and seeders. Seeders are where this package pays off most — one array per record covering all locales keeps fixtures readable. Do the same for CSV imports: one row per entity, columns per locale, rather than one row per translation.

Gotchas and Tips

  • No maintenance signal. Zero stars and zero dependents means no community bug reports. Before adopting, read the trait for saved/deleting event hooks and confirm orphaned translation rows are cleaned up on delete. If they aren't, add a deleting observer yourself.
  • Mass assignment. Translation writes often go through the trait's setter, not fill(). If your $translatable fields are also in $fillable, you can end up writing the parent column and the translation row. Check which one wins.
  • Uniqueness constraints. A slug unique index on the parent table is wrong once slugs are per-locale. Make sure the index is (locale, slug). This is the single most common source of SQLSTATE[23000] after switching to a translatable package.
  • Cache invalidation. If the package caches rendered translations (some do), verify the cache is flushed on save and on locale config change. Stale copy after an edit is the most confusing bug class here, and it looks like a browser cache problem.
  • Testing. Assert fallback explicitly — set the locale to one with no translation and assert the expected value, including the empty-string case. Also assert that saving a locale you didn't touch doesn't wipe the others; partial updates are where these traits lose data.
  • Extension points. If the trait is used, you can override its methods in the model. If behavior lives in a repository/service class bound in the provider, you can swap it via container binding. Prefer the latter for anything you'd otherwise copy-paste across models.
  • Debugging. DB::listen() or Laravel Telescope will show immediately whether reads are hitting the translation table per row. Do this once on a listing page before shipping.
  • Exit cost. Because the package is unmaintained-by-popularity, wrap access behind your own accessors and scopes rather than calling trait methods throughout Blade and controllers. If you later migrate to spatie/laravel-translatable or astrotomic/laravel-translatable, the swap is contained to those wrappers plus the migration.
  • Compare alternatives before committing. Spatie and Astrotomic are the well-trodden paths with documented fallback, whereTranslation, and sluggable integration. Choose organi/translatables only if its storage model matches your schema better or you're already inside the Organi ecosystem — otherwise you're taking on maintenance risk for parity functionality.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console