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

Translatable Laravel Package

terranet/translatable

A Laravel package for adding multilingual attributes to Eloquent models. Store translations per locale, retrieve values with automatic fallback, and manage translated fields without duplicating your schema—useful for building translatable applications quickly.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin first. The package was last released in March 2019 and has no dependents, so before writing any code confirm it will even install against your Laravel version:

composer require terranet/translatable
composer show terranet/translatable   # check requires: laravel/framework, php

If Composer resolves to an old illuminate/* constraint, either run it on a matching legacy app or fork it and bump the constraints — do not try to shoehorn it into Laravel 10+ without reading composer.json first.

Where to look first, in this order:

  1. src/ for the main trait (likely Translatable or HasTranslations) — this is the whole API surface you'll use daily.
  2. The service provider — check whether it's auto-discovered (extra.laravel.providers) or needs manual registration in config/app.php (very likely for a 2019 package).
  3. Any config/translatable.php and database/migrations/ stubs — publish them before touching models:
php artisan vendor:publish --provider="Terranet\Translatable\TranslatableServiceProvider"
  1. The README examples for Admin Architect — this package is a satellite of Terranet's admin panel, so most of its docs assume you're rendering translation tabs in that UI.

First use case: pick one model with two or three short text fields (a Page with title and body is ideal), add the trait, declare the translatable attributes, run the migration, and verify you can write and read the same field in two locales. Only then wire it into forms.

Implementation Patterns

Trait + attribute list. The core pattern is the same as every Laravel translatable package: a trait on the model and a whitelist of translated columns.

use Terranet\Translatable\Translatable;

class Page extends Model
{
    use Translatable;

    protected $translatable = ['title', 'body'];
}

Confirm the exact property name from the trait source — $translatable vs $translatedAttributes is the single most common integration mistake, and getting it wrong fails silently (you just get the raw column back).

Two storage models to plan around. Decide early whether translations live in JSON columns on the parent table or in a side translations table. JSON columns mean simpler migrations and no joins, but you lose indexability and per-locale constraints. Side tables mean a join and a whereHas('translations', ...) scope for every filtered query, but they scale better and let you add slug uniqueness per locale. The package's migration stub will reveal which one it assumes.

Locale switching belongs in middleware, not in controllers. Set the locale once in a SetLocale middleware (app()->setLocale(...) from session/user/header), then let models resolve translations implicitly. Avoid calling ->translate('ro') in random controllers — that's how you end up with mixed-locale output in one response.

Admin Architect integration. If you're on Admin Architect, the payoff is form rendering: you declare the field once and the package generates per-locale inputs (usually as tabs). Check the form/filter classes it ships and use its field types rather than hand-building <input name="title[ro]"> markup, otherwise you'll fight the mass-assignment and validation conventions it expects.

Validation per locale. Build a FormRequest that expands rules across your supported locales instead of writing them three times:

protected function rules(): array
{
    $rules = ['slug' => 'required|alpha_dash'];
    foreach (config('app.locales') as $locale) {
        $rules["title.$locale"]  = 'required|string|max:255';
        $rules["body.$locale"]   = 'nullable|string';
    }
    return $rules;
}

API output. For a JSON API, either flatten with fallback ($page->title after setting locale) or expose the whole translation map ($page->translations) so the frontend can switch languages client-side. Pick one and be consistent; mixing the two creates subtle caching bugs in HTTP responses.

Eager loading. If translations are a relation, add with('translations') to index queries — a 25-row listing with lazy-loaded translations is 26 queries, which is exactly the kind of thing that ships to production unnoticed.

Gotchas and Tips

  • Maintenance risk is the headline. Two stars, zero dependents, no release since 2019. Treat it as vendored code: fork it into your org, or be prepared to. Don't build core architecture on it without reading the trait end-to-end — it's small enough to audit in an hour.
  • Silent no-ops are the failure mode. Wrong trait property name, wrong locale key, or a missing migration all render as "the translation just doesn't save." When something looks broken, dd($model->getAttributes()) immediately after save() to see whether the write happened at all.
  • Fallback semantics differ. Some implementations return null, some return the fallback-locale string, some return ''. Decide what your UI does with an empty translation ('' usually means "show fallback", null means "not translated yet") and enforce it in one place, not in every Blade template.
  • JSON columns need MySQL 5.7+ / MariaDB 10.2.7+. On older databases the migration fails or silently degrades to TEXT. If you must support both, use LONGTEXT with a json cast — but then you can't use whereJsonContains.
  • Searching translated content is the hard part. LIKE '%term%' against a JSON blob works but is slow and locale-blind. For real search, index into Scout/Meilisearch with one document per locale, or add generated columns for the locales you actually query on.
  • Locale bleed in queues, commands, and tests. app()->getLocale() is whatever the process default is — a queued job that renders a translated email will render it in config('app.locale') regardless of the user's preference. Pass the locale into the job and set it explicitly; same in feature tests, where you should assert against a locale you set yourself rather than the app default.
  • Cache invalidation. If you cache rendered pages or API responses, remember the cache key must include the locale. This bug shows up as "the site is in Romanian for everyone" after a deploy.
  • Extension points to prefer over patching. Override the accessor/mutator in your own model, or swap the translation repository binding in a service provider, rather than editing vendor/. Given the package's age, you will need a local change eventually — make it a fork, not a vendor/ edit that disappears on the next composer install.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core