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

marshmallow/translatable

A Laravel package for adding translations to Eloquent models. Use a trait to mark translatable attributes, store per-locale values, and retrieve localized content with fallback support, keeping multilingual model data simple and integrated with Laravel.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and publish — this package ships a service provider and at least one migration (a translatables polymorphic table). Start with:

    composer require marshmallow/translatable
    php artisan vendor:publish --provider="Marshmallow\Translatable\TranslatableServiceProvider"
    php artisan migrate
    

    Check config/translatable.php (or similarly named) for the locale list and the morph key naming — that config is the first thing to read, not the README examples.

  2. Where to look first — the migration and the trait are the whole contract. Read database/migrations/*_create_translatables_table.php to see the exact columns (translatable_type, translatable_id, locale, key, value — verify names) because every debugging session later ends there. Then read the trait to see which model methods it injects (translate(), getTranslation(), attribute accessors, magic __get).

  3. Wire up one model — add the trait to the model you want translated, then add the Nova field to that resource. Do this for a single throwaway model first:

    class Article extends Model {
        use \Marshmallow\Translatable\Traits\Translatable;
    }
    
    public function fields(Request $request) {
        return [
            // ... other fields
            \Marshmallow\Translatable\Fields\Translatable::make('Title')
                ->locales(['en', 'nl', 'de']),
        ];
    }
    
  4. First real use case — a CMS-style resource where editors need per-locale title, slug, and body on the same record, and you want the site (not Nova) to resolve app()->getLocale() at render time. That is the package's sweet spot: content tables stay clean, translations live in one side table.

Implementation Patterns

Keep the translation column off your main table. The entire premise is that articles has no title column. Your Nova resource field name must match the translation key, not a DB column. If you leave the original column in place you will get silent shadowing between the real attribute and the translated one — pick one and delete the other.

Reading translations in app code. The usual shape is a translated accessor with fallback:

$article->translate('title', 'nl') ?? $article->title;   // verify method signature
app()->getLocale();                                      // if the trait auto-resolves

Decide early whether you want automatic locale resolution via the trait (convenient, but it makes $article->title ambiguous in queued jobs and console commands where app()->getLocale() is en by default) or an explicit translate($key, $locale) call everywhere. For APIs, explicit wins.

Locale switcher in Nova. The cleanest workflow with this kind of field is one Nova form with a locale selector (or tabs) that swaps which translation set the field group is editing, rather than N duplicated fields. If the field supports per-locale configuration, group your translated fields into a Panel/Tab per locale — it keeps the edit screen readable and matches how editors think.

Eager loading is mandatory. Because translations are a separate relation, a listing of 50 articles with 3 translated fields is 150+ queries without it. Either eager load the relation in your repository/controller, or cache the resolved translation map per request. Check whether the trait exposes a withTranslations() scope; if not, add a small helper on your model base class.

Index columns. Nova index rows resolve translated values one row at a time. For any table over ~100 rows, either display the base-locale value via a computed field or accept the query cost knowingly. Searching/sorting on translated values needs a join — this package will not give you whereTranslation() for free unless it's documented; write the join yourself against translatables.

Migration and import path. Existing content needs backfilling: loop your records in chunks and write each existing column into the translatables table for the base locale, then drop the old columns in a second migration. Do it as two deploys (write both, then cut over) so you can roll back.

Saving in Nova. Translated fields save through the resource's store/update lifecycle into the side table. If you have observers, saving/saved hooks on the parent model will not see the translation payload — hook the translated model or the Nova lifecycle event instead, or your slugs/audit logs will silently miss translations.

Test it explicitly. Feature-test both locales rather than trusting the field: create via Nova (or novaPost), assert the translatables rows, then assert the front-end resolution and the fallback when a locale is missing.

Gotchas and Tips

  • Two stars, zero dependents. Treat the README as a hypothesis, not documentation. Budget an hour to read the actual trait and field class before committing. Pin the version in composer.json — with one maintainer and a Feb 2026 release, an unpinned minor bump can change the field API.
  • Nova version compatibility. Nova field APIs shift between majors (4.x → 5.x). Confirm the nova constraint in the package's composer.json matches your install, and smoke-test one resource's create/edit/index after any Nova upgrade.
  • app()->getLocale() in the wrong context. Queue workers, Artisan commands, and Nova itself (which uses its own locale, not the front-end one) will all resolve to your default. Never rely on implicit locale in those paths — always pass the locale explicitly.
  • Fallback semantics are yours to define. Most implementations return null for a missing translation rather than falling back. Wrap it: translate($key, $locale) ?? translate($key, config('app.fallback_locale')) ?? $original. Missing this produces empty headings in production, not an exception — the quietest possible failure.
  • Caching. If you cache rendered models or API responses, the translation relation must be part of the cache key or invalidated on save. translatables rows are not covered by the parent model's updated_at unless the trait touches it — verify, and if it doesn't, $model->touch() after writing translations.
  • Search. WHERE title LIKE ? will not find translated titles. Either denormalize a searchable column on the parent for the base locale, or index translations into Scout/Meilisearch with locale as a filterable attribute.
  • Deletes. Confirm the package cleans up translatables rows on model delete (cascade or observer). Orphaned polymorphic rows are the classic bug here; add a scheduled prune if unsure.
  • Extension point. The polymorphic translatable_type/translatable_id shape means you can bypass the package entirely for bulk operations — DB::table('translatables')->upsert(...) in a job is often faster and clearer than looping model saves. Just keep the morph class string exact (watch morphMap if you alias models).
  • Debugging tip. When a translation "doesn't save," check the morph type first: a custom Relation::morphMap() alias vs. FQCN mismatch silently writes rows the trait then cannot find.
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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource