pixelpeter/filament-language-tabs
Filament plugin that adds language tabs to forms, letting you edit translatable fields per locale in a clean tabbed interface instead of stacking every translation. Makes managing multilingual content in Filament admin panels simpler.
This package is a thin Filament form component that turns a flat list of translatable fields into one tab per locale, so you don't hand-write Tabs::make()->tabs(['en' => [...], 'de' => [...]]) in every resource.
Minimal setup
composer.json for the supported Filament major version before committing):
composer require pixelpeter/filament-language-tabs
php artisan vendor:publish --tag="filament-language-tabs-config"
spatie/laravel-translatable (JSON column + $translatable array), but verify from the package's composer requirements whether that's a hard dependency or just the assumed convention.Where to look first
LanguageTabs::make()->schema([...]), ->tabs([...]), or similar) is the one thing you must copy verbatim.locales, default_locale, and label formatting.src/) — it's small enough to read end-to-end in ten minutes, which is the real safety net for a 13-star package.First use case
Take one resource with two or three translatable fields (name, slug, description) and replace the manual tabs block with the package component. Compare the rendered form, the saved JSON payload, and the validation errors per locale against your old implementation. That single resource tells you whether the package's locale-key convention matches your Spatie config — the most common source of silent breakage.
Wrap only the translatable fields. Keep slugs, status flags, media, and relations outside the language tabs. The component is a grouping device, not a form framework; mixing non-translatable fields inside forces you to duplicate them per locale and creates confusing state paths.
// Typical shape — confirm method names against the README
LanguageTabs::make()
->schema([
TextInput::make('name')->required(),
Textarea::make('description'),
])
->columnSpanFull(),
Extract a shared schema builder. If several resources translate the same fields, put the field definitions in a static method or a small TranslatableFields helper and pass them into the component. This keeps locale handling in one place when you add a language next quarter.
Pair with table-side handling. This package only solves the form. For the list view you still need locale-aware columns — TextColumn::make('name.de'), a fallback accessor, or a dedicated translatable-column plugin. Decide early whether the table shows the app locale or the default locale; users notice inconsistency between form tabs and table output immediately.
Validation per locale. Add ->required() only on the default-locale instance if you don't want to force every translation. If the component renders the same field name across tabs, per-locale rules like name.de => max:255 belong in the resource's getValidationRules() or a FormRequest — check how the package namespaces state before writing them.
Reuse inside repeaters and builders with care. A language-tabs block inside a Repeater item works in simple cases but multiplies tab count fast. Prefer a flat form for translated content, and use relationship repeaters (one row per translation) only if you've abandoned JSON columns.
Testing. Filament's Livewire test helpers (fillForm, assertFormSet) work if the component exposes state under predictable paths. Write one test that fills the default locale and one that fills a secondary locale, then assert the JSON column contents — this catches most regressions when you upgrade Filament.
Locale key mismatch is the #1 failure. The package's locale list, config/app.php locales, and your Spatie translatable setup must agree on keys (de vs de_DE vs de-DE). If tabs render but data saves empty, dump the dehydrated state before touching anything else.
Default-locale fallbacks. If the default locale tab is empty but the model has a fallback value configured, the form may show blank while the front end shows translated content. Decide explicitly whether the form pre-fills from fallback and document it — otherwise editors "lose" content on save.
Migration and cast hygiene. JSON columns need the array (or Spatie's translatable cast) plus the field in $translatable/$fillable. A missing cast produces "Array to string conversion" on save, which looks like a package bug but isn't.
Filament major-version compatibility. With 13 stars, expect release cadence to trail Filament. Pin the package version in composer.json, check the changelog before running composer update, and keep a note of the last Filament version you verified. If it breaks on a Filament upgrade, the component is small enough to copy into app/Filament/Components and own — that's the realistic escape hatch.
Nesting and layout quirks. columnSpanFull(), Grid columns, and section headings often need to sit on the wrapper, not the inner fields. If tabs render cramped or half-width, adjust the wrapper first.
Hidden or disabled locales. For per-tenant or per-plan language sets, drive the locale list from config or a service rather than hardcoding; check whether the package resolves locales at boot or per-request, since a cached schema can freeze the list.
Extension points to check in src/: the tab-label formatter (for flags/icons/uppercase codes), the method that maps a field name to a locale-specific state path (override this if your column naming differs), and any Blade view you can publish. These are the three places worth customizing before considering a fork.
When not to use it. If you only have one translatable field, or you already have a working Tabs block, the abstraction buys little and adds an upgrade dependency. The package earns its place when you have three-plus locales and multiple resources with the same translated field set.
How can I help you explore Laravel packages today?