fnx-software/filament-astrotomic
A Filament plugin for Astrotomic Translatable models that simplifies multilingual content management. Edit translations directly in Filament forms and tables with locale-aware fields, tabs, and validation, reducing custom code and making translated records easy to maintain across your admin panel.
This package extends Filament's form/table/infolist components so translated model attributes work out of the box with spatie/laravel-translatable-style models. You get three main building blocks: TranslatableSelect (form field), TranslatableColumn (table column), and TranslatableEntry (infolist entry).
TranslatableSelect on a relationship whose label lives in the translation table:TranslatableSelect::make('department_id')
->label('Department')
->translatableRelationship('department', 'name')
->searchable()
->preload();
In v2.1.0 the recommended starting point for anything beyond a single translated attribute is the new composite-label API, since it removes the old assumption that translated attributes exist as base-table columns:
TranslatableSelect::make('person_id')
->translatableRelationship('person', 'first_name')
->translatableLabelUsing(fn (Person $record, string $locale): string => collect([
$record->translate($locale)?->first_name,
$record->translate($locale)?->father_name,
$record->translate($locale)?->last_name,
])->filter()->implode(' '))
->translatableSearchAttributes(['first_name', 'father_name', 'last_name']);
Where to look first: the HasTranslatableRecordLabel concern (new in 2.1.0) if you want the same label logic reused across multiple resources instead of repeating closures.
Single translated title attribute. The baseline pattern: pass the relationship name and the translated title attribute. Option loading, labels, and search now resolve through translated records rather than Filament's default relationship title column.
Composite translated labels. For names built from multiple translated parts, pair translatableLabelUsing() with translatableSearchAttributes(). The label closure receives the related record and the active locale, so you can call $record->translate($locale) yourself and filter()->implode(' ') to gracefully skip empty parts.
Shared label behavior via the concern. Use HasTranslatableRecordLabel when several selects/infolists need identical label composition. Implement the label logic once on the model (or a dedicated concern), then keep field definitions thin:
TranslatableSelect::make('person_id')
->translatableRelationship('person', 'first_name')
->translatableLabelUsing(fn (Person $r, string $locale) => $r->translatableRecordLabel($locale));
Keep search and label in sync. Whenever you customize the label, add translatableSearchAttributes() covering the same attributes. Otherwise users see a full name but can only search by the first part — a common support ticket.
Query customization. Use the third modifyQueryUsing argument on translatableRelationship() for scoping and ordering. This is the sanctioned extension point and still works in 2.1.0:
->translatableRelationship(
relationship: 'department',
titleAttribute: 'name',
modifyQueryUsing: fn ($query) => $query->where('active', true)->orderBy('order'),
)
Locale-aware forms. Because the label closure receives the active locale, avoid caching composed labels across locales. Treat the closure as pure and cheap; it runs per option render.
Mix with tables/infolists. TranslatableColumn and TranslatableEntry are unchanged in 2.1.0 — keep using your existing setup for list/detail views, and rely on TranslatableSelect only for form input.
Unknown column errors. If you previously hit Unknown column 'related_table.name' in 'field list', 2.1.0 fixes it by no longer requiring translated attributes to exist on the base table. Upgrading is the fix; you don't need a workaround query anymore.titleAttribute is still required. Even with translatableLabelUsing(), the signature expects a title attribute; keep it pointing at a sensible primary translated field so fallbacks and search defaults behave.translatableSearchAttributes() explicitly, including base-table attributes if you want them searchable too.$record->translate($locale) can return null; always null-safe chain (?->first_name) and filter() before implode() to avoid " " labels or stray spaces.modifyQueryUsing(fn ($q) => $q->with('translations')) when option counts are large.orderBy('name')) may still fail. Order by base-table columns, or join translations deliberately in modifyQueryUsing.TranslatableSelect changed in 2.1.0. Don't expect the new label/search APIs on TranslatableColumn or TranslatableEntry.modifyQueryUsing scopes first, then confirm the active locale actually has translations for the record — a missing translation row silently produces blank labels.How can I help you explore Laravel packages today?