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

Filament Translatable Laravel Package

jegex/filament-translatable

Filament Translatable adds seamless multi-language field editing to Filament forms, letting you manage translations across locales without leaving the admin panel. A lightweight helper for Laravel apps that need clean, simple, per-locale content editing.

View on GitHub
Deep Wiki
Context7

Getting Started

jegex/filament-translatable is a thin Filament layer over spatie/laravel-translatable. Instead of hand-rolling locale tabs and title[en] state paths in every resource, you wrap a set of fields once and get per-locale inputs that dehydrate into the JSON shape Spatie expects.

Prerequisites (check these before anything else):

  • spatie/laravel-translatable installed, model using HasTranslations with a populated $translatable array.
  • The underlying columns are json (or text) — not string. A varchar column is the single most common reason a first attempt "silently doesn't save".
  • A Filament version the package actually supports. Confirm against the README for the tag you install; the constraint is your first compatibility gate.

Install:

composer require jegex/filament-translatable
php artisan vendor:publish --tag="filament-translatable-config"   # if offered

If it ships a Filament plugin class, register it in your PanelProvider (->plugin(...)); otherwise it's a form component you import directly.

First use case — get one field working end to end: take a Post model with title and body translatable into en/fr/es. Wrap those two fields in the package's translatable container in PostResource::form(), load the edit page for an existing record, change the fr title only, save. Then verify, not just eyeball the UI:

$post->refresh()->getTranslations('title'); // ['en' => '...', 'fr' => '...', 'es' => '...']

If that array is right, the wiring is correct and everything else is presentation.

Where to look first: the README's form example, the package config file (locale list + fallback), and src/ for the container class and service provider. The source is small — reading the container's dehydrate/hydrate methods answers most state-path questions faster than the docs.

On v1.0.6: this is a patch bump from v1.0.5 with no published notes beyond the compare link. There are no announced breaking changes, deprecations, or features. Nothing to migrate; treat it as a routine pin.

Implementation Patterns

One wrapper per translatable attribute group. Keep inner field names identical to the model's $translatable entries — any renaming and the dehydration mapping breaks quietly.

Decide the editing model per resource, up front. Two patterns, and mixing them is where teams get confused:

  • All locales at once — one tab per locale in a single form. Right for content editors and marketing copy.
  • Current panel locale only — set the locale in middleware/on the panel, and let authors edit one language at a time. Right for large teams where translators shouldn't see other languages.

Pick one convention per resource and document it. A resource that half-does both produces "I edited it but the other language changed" bug reports.

Table columns need an explicit locale. Translated attributes are arrays; a plain TextColumn::make('title') will either error or render nothing useful:

TextColumn::make('title')
    ->getStateUsing(fn ($record) => $record->getTranslation('title', app()->getLocale())),

Validation belongs inside the wrapper. Rules applied to inner fields fire per locale. The usual business rule is "fallback locale is required, others optional" — express that with a conditional ->required(...) rather than marking every locale required, which translators will hate.

Extract the schema. Put the translatable field group in a static function translatableFields(): array on the resource (or a trait) so create, edit, and relation managers can't drift apart.

Always pass locale explicitly in actions, exports, API resources, and Blade partials: $post->getTranslation('title', $locale), never the ambient $post->title.

Testing: the wrapper nests state under the locale key, so Livewire tests fill ['data.title.en' => 'Hello', 'data.title.fr' => 'Bonjour'] and then assert on the DB JSON or getTranslations(). Add one test that saves a partial form (only one locale filled) — that's the path that regresses.

Gotchas and Tips

  • Array-to-string conversion on table columns, SelectColumns, and anywhere Filament assumes a scalar. Symptom: htmlspecialchars(): Argument #1 must be of type string, array given. Fix with getStateUsing.
  • Search and sort don't work on JSON columns out of the box. Use ->searchable(query: fn ($q, $search) => $q->where('title->en', 'like', "%{$search}%")), and if you sort on translations at scale, add generated columns with indexes — JSON extraction sorts are table scans.
  • unique validation is misleading. Rows are shared across locales, so a standard unique:posts,title checks the whole JSON blob, not "unique within fr". You need a custom rule. This is the most common post-launch data-quality bug with any Spatie-translatable UI.
  • Fallback locale masks empty inputs. $post->title returns the fallback when the requested locale is blank, so a field that looks populated may have no fr value at all. Read getTranslations() whenever the distinction matters (completeness reports, publishing gates).
  • Adding a translatable attribute later requires a backfill in the new shape — {"en": "old value"}, not "old value". A plain string in a JSON column will blow up hydration on edit.
  • Nested components (Repeater, Builder) inside the translatable container produce deep state paths. Test hydration on an existing record before shipping; create-only testing hides the bug.
  • File uploads inside a per-locale tab need verification of dehydration. If in doubt, keep one file per record (MediaLibrary) outside the wrapper.
  • Duplicated rich text editors are expensive. Three locales × TinyMCE/Tiptap is a heavy DOM and a heavy Livewire payload. Consider trimming the locale list in tabs to what editors actually touch.
  • Keep the package locale list and app.supported_locales in sync, and make sure the fallback locale matches Spatie's fallback_locale. Drift here produces "why is this English" reports that look like package bugs but are config bugs.
  • Debug with $livewire->form->getState() (test or Tinker) before blaming the package — it shows you exactly what shape was dehydrated and whether the problem is the wrapper or your model.
  • Extension points: publishable views/config for the tab UI; subclassing the container to add behaviours. If you subclass, pin the exact version — patch releases (like v1.0.6) carry no notes, so a ^1.0 constraint plus a subclass is how you get a surprise. Pin, run your translatable-field test, then bump.
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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log