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 Fields Laravel Package

outerweb/filament-translatable-fields

A Filament plugin that adds translatable form fields, letting you edit translations for each locale in tabs within your resources. Works with spatie/laravel-translatable to keep multilingual content organized and easy to manage in the admin panel.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up the three moving parts.

composer require outerweb/filament-translatable-fields
php artisan vendor:publish --tag="filament-translatable-fields-config"

The config is the only place that defines which locales get tabs:

// config/filament-translatable-fields.php
return [
    'locales' => [
        'en' => 'English',
        'nl' => 'Nederlands',
        'de' => 'Deutsch',
    ],
];

The package assumes the underlying model already uses spatie/laravel-translatable — it does not set that up for you:

use Spatie\Translatable\HasTranslations;

class Post extends Model
{
    use HasTranslations;

    public array $translatable = ['title', 'body'];
}
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->json('title');
    $table->json('body')->nullable();
    $table->timestamps();
});

First use case — swap a plain TextInput for the wrapper in your Resource form:

use Outerweb\FilamentTranslatableFields\Filament\Fields\Translatable;

Translatable::make('title')
    ->fields([
        TextInput::make('title')
            ->required()
            ->maxLength(255),
    ])
    ->columnSpanFull(),

The component renders a Filament Tabs with one tab per configured locale, and each inner field is registered under title.en, title.nl, etc. Filament's dot-notation state handling nests those into ['en' => ..., 'nl' => ...], which is exactly the shape Spatie's mutator expects on save. That convention is the entire package.

Where to look first: the README example, then src/Filament/Fields/Translatable.php (the package is essentially one small class plus a config and service provider — read it in five minutes), then config/filament-translatable-fields.php. Confirm whether your installed version uses ->fields([...]) or ->schema([...]) for the inner components; small packages like this occasionally rename methods between minor releases, and the README on main may not match the tag you installed.

Implementation Patterns

One wrapper per translatable column, not per locale. Group related columns into Sections and let each Translatable own its own tab strip:

Section::make('Content')
    ->schema([
        Translatable::make('title')
            ->fields([TextInput::make('title')->required()->maxLength(255)]),
        Translatable::make('body')
            ->fields([RichEditor::make('body')->required()]),
    ])
    ->columnSpanFull(),

Pull it into a helper on the Resource so 10 resources don't drift:

protected static function translatableText(string $name, string $label): Translatable
{
    return Translatable::make($name)
        ->label($label)
        ->fields([TextInput::make($name)->required()->maxLength(255)]);
}

Mixed field types work as-is. Textarea, RichEditor, MarkdownEditor, Toggle, Select, FileUpload all go inside ->fields([...]) — they are ordinary Filament components; the wrapper only renames their state path. This is why per-locale FileUpload with a locale-specific ->directory("posts/{$locale}") is the usual pattern: build the inner fields in a loop over your locale config instead of hand-writing three near-identical components.

Validation belongs on the inner field. ->required() on TextInput::make('title') ends up validating data.title.en, data.title.nl, etc. — i.e. required in every locale. If you only need the default locale required and the rest optional, apply ->required() conditionally per locale field, or add a closure rule on the wrapper that asserts at least one locale is non-empty.

Tables are a different code path — don't force the wrapper there. TextColumn::make('title') already works because Spatie's accessor resolves the current app locale, and ->searchable() goes through Spatie's where('title->en', 'like', ...) query scope. If you want a per-locale table column, use TextColumn::make('title.en') only if you've verified your Filament version hydrates it from attributesToArray() — otherwise fall back to ->formatStateUsing(fn ($state) => $state[app()->getLocale()] ?? '') on the plain column.

Slugs per locale. Pair each Translatable title with a per-locale slug field built the same way, and generate slugs in ->afterStateUpdated() on the inner TextInput using Str::slug($state). Don't try to auto-generate across locales from a single field — the tabs make that confusing and editors will fight it.

Repeaters and Builders generally work if you keep the wrapper inside the item schema and use a normal relative name (title), because Filament resolves the repeater's state path before the locale suffix is appended. Test one save round-trip before committing to it in a large form.

Gotchas and Tips

  • The column.locale naming is load-bearing. If your column isn't in $translatable, or you renamed the wrapper to something that doesn't match a real JSON column, saving silently writes title.en as a top-level attribute instead of nesting — the form looks fine and the data is wrong. Debug with dd($model->getTranslations('title')) after a save, not $model->title.
  • ->required() on every locale is usually the wrong default. For long-form fields (body, meta description), forcing all locales before publish blocks authors. Require the default and treat the rest as optional, with a badge or hint on the default tab.
  • Unique validation on JSON columns breaks. ->unique(ignoreRecord: true) on title.en will try WHERE title.en = ? and fail or return nonsense. Use a Rule::unique closure against whereJsonContains('title->en', $value) (or a generated column) instead.
  • Fallback overwrites. Spatie returns the fallback locale when a translation is missing. An empty tab can therefore display the English text while state is null — if an editor saves without touching it, you may persist "" and shadow the fallback. Filter empty strings out of the dehydrated state if that matters, or make each tab visibly empty.
  • Nested tabs get ugly fast. The package gives you one tab strip per column. If your real UX need is “show all fields for Dutch at once,” this package is the wrong shape — build that manually with Filament's native Tabs and TextInput::make('title.nl') fields. Recognizing this early saves a rewrite.
  • Locale order comes from config. Put your default locale first so editors land on the tab they actually fill in. A locale in config but not in the model's data yields an empty tab; a locale on the model but missing from config is silently unreachable in the form.
  • Component count scales. Ten locales × eight translatable fields = 80 inner components plus tab chrome per form render. Fine for typical editorial models, noticeable on wide resources with heavy RichEditors — lazy-load or scope which columns are translatable.
  • Pin the version and re-test on Filament upgrades. This is a ~36-star, single-maintainer package with a thin surface area over Filament internals (Tabs, state path resolution, dehydrateState). A composer.json constraint like "outerweb/filament-translatable-fields": "^1.0" plus a smoke test in CI (create record, save two locales, assert getTranslations()) is cheap insurance.
  • Extension points are minimal by design. If you need “required on default locale only,” a fallback indicator, or a “copy from English” button, subclass Translatable and override how ->fields() builds the inner components — don't fight the config file, and don't expect hooks that aren't there.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console