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

happenv-com/filament-translatable

Filament Translatable provides a complete solution for managing multilingual content in Filament admin panels, adding intuitive tabbed translatable form fields with multiple translation backends, configurable locales, and support for Filament 3, 4, and 5.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require happenv-com/filament-translatable
php artisan filament:assets

Register the plugin in your panel provider and decide the locales up front — this is the one config decision everything else inherits:

use Happenv\FilamentTranslatable\FilamentTranslatablePlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(
            FilamentTranslatablePlugin::make()
                ->locales(['en' => 'English', 'pl' => 'Polish', 'fr' => 'French'])
                ->defaultLocale('en')
                ->displayFlagsInLocaleLabels(true)
        );
}

Before touching the UI, make the model translatable in the backend the package expects. With Spatie (the default) that means the HasTranslations trait plus a JSON column:

class Post extends Model
{
    use Spatie\Translatable\HasTranslations;

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

// migration
$table->json('title')->nullable();

First use case — converting a single field into a tabbed multilingual input:

TextInput::make('title')
    ->requiredDefaultLocale()
    ->translatable()

That's the whole loop: model column, plugin locales, ->translatable(). Look at the generated Translations component (the macro swaps the field's context to it) as your first debugging surface if state looks wrong.

Implementation Patterns

Macro for the 80% case. ->translatable() on a single field is the default. Remember the context switch: after ->translatable() you are talking to Translations, not TextInput, so all field-specific configuration must come first and all tab-layout configuration after:

TextInput::make('price')
    ->requiredDefaultLocale()
    ->decorateTranslationField('pl', fn (TextInput $f) => $f->suffix('PLN'))
    ->decorateTranslationField('en', fn (TextInput $f) => $f->prefix('USD'))
    ->translatable()
        ->vertical()
        ->displayFlagsInLocaleLabels(true)
        ->flagWidth('24px');

Translations component for grouped fields. When several fields share a locale context (title + slug + body), build one component around a schema instead of chaining macros. This keeps tab switching coherent and gives you one place to configure labels:

Translations::make('translations')
    ->schema([
        TextInput::make('title'),
        TextInput::make('slug'),
        RichEditor::make('body'),
    ])
    ->prefixLocaleLabel()
    ->suffixLocaleLabel()
    ->fieldTranslatableLabel(fn ($field, $locale) => __("fields.{$field->getName()}", locale: $locale))

Per-component locale overrides. Editors rarely need every locale. Override locally rather than forking the plugin config:

Translations::make('translations')->locales(['en', 'es'])

Dynamic locale lists. A closure keeps locales in sync with a languages table, so adding a language is a DB row, not a deploy:

FilamentTranslatablePlugin::make()->locales(fn () => Language::pluck('code', 'name')->all())

Per-locale actions. Custom actions inside a tab receive the current locale — a good hook for "copy from English" or "auto-translate":

Translations::make('translations')
    ->schema([...])
    ->actions([ /* access $locale inside the action */ ]);

Mode switching is granular. translationMode(TranslationMode::Astrotomic) is settable on the plugin, a component, or a single field. Use it only when you have a mixed legacy codebase; otherwise set it once on the plugin.

Gotchas and Tips

  • Order matters, and it fails silently-ish. ->required() after ->translatable() targets the component, not the field. Call field methods before the macro. The README's caution is the single most common source of "my validation doesn't work" bugs.
  • Don't call ->translatable() inside a Translations schema. It's already translated; nesting produces duplicated state paths and confusing tab counts.
  • Each Translations component needs a unique name. Two components both named translations will collide on action resolution and state keys. Name them by intent (translations_main, translations_seo).
  • Required semantics are per-locale, not global. requiredLocale('en') and requiredDefaultLocale() are the correct tools. ->required() on the field will require every locale tab at once, which is rarely what you want.
  • Table columns don't translate themselves. The package is a form-layer tool. Rendering $record->title returns the app-locale value; for admin tables either use a locale switcher in the panel, or ->getStateUsing(fn ($record) => $record->getTranslation('title', 'pl')), or add a custom column that iterates locales.
  • Astrotomic changes your schema shape. Translations live in a related table, not a JSON column — so eager loading (with('translations')), migrations, and any direct query filtering are entirely different from the Spatie path. Use the package's Happenv\FilamentTranslatable\Traits\AstrotomicTranslatable trait, not the vendor's.
  • Searching/filtering translated JSON requires JSON syntax. whereJsonContains('title->en', ...) or driver-specific JSON path queries on MySQL/Postgres. Full-text search across all locales means either whereRaw with JSON unnesting or a separate indexed search table.
  • Run php artisan filament:assets after every update — flag icons and the component's JS are published assets, and stale assets are the usual cause of "tabs render but nothing switches."
  • Debug via state, not the DOM. ->getState() on the component (or dd($this->form->getRawState()) in a Livewire test) tells you exactly which field.locale keys are being hydrated. If a locale key is missing, it's a locales-config mismatch, not a rendering bug.
  • Extension points worth knowing: preformLocaleLabelUsing(fn (string $locale, string $label) => "[{$label}]") for label formatting, decorateTranslationField() for per-locale field chrome, and the $field injection in prefixLocaleLabel() to apply labels selectively rather than to every field.
  • Test the save path in a Livewire test. Fill one locale, assert the JSON column, and assert a requiredLocale() violation for the others — this catches trait/cast mistakes that are invisible in the browser until production data looks empty.
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.
quimgc/tasks
sistem-atc/bank-sdk
core45/laravel-cookieconsent
projectsend/community-modules
ovac/laravel-hubtel-payment
capimail/capimail-php
shiroamada/green-api-laravel-notification
shiroamada/ultrasms-laravel-notification
esazykin/laravel-clickhouse
nbrabant/laraadmin
tobya/saloonfire
kjmtrue/laravel-debugbar
hexincorp/debugbar
razorbacks/laravel-shibboleth
adilazhari/laravel-idempotency
mamitech/laravel-slow-query-log
mamitech/laravel-http-logger
medeirosrafael/docxmustache
webdevlabs/phreak
vis/log-envelope