
Summary
Locale-aware schema layout built on SegmentTabs (iOS-style segmented tabs, same visual language as SegmentControl). Clones one or more field templates into per-locale tabs with automatic state paths, JSON hydration, and optional Spatielaravel-translatable support.
Designed as a first-party, extensible alternative to third-party translatable tab packages — with explicit extension points (localeFieldUsing, storageAttributeUsing, tab/field modifiers) and no external plugin dependency.
Not the same as TitleSlugField translations. TitleSlugField provides a narrower use case: translatable titles with a single shared slug and slug-source-locale sync. Use TranslatableFields for any generic translatable attribute (body, excerpt, metadata, …).
Basic usage — standalone component
Register field templates with->schema(). Each template is cloned once per locale tab.
title.ar / title.en (and body.ar / body.en) into nested arrays on the model. On edit, values are hydrated from JSON columns automatically.
Basic usage — field macro
Wrap a single field in locale tabs without declaringTranslatableFields explicitly. The macro returns the TranslatableFields component (replace the field in your schema with the return value).
Macro with inline modifiers
Legacy aliases
For projects migrating fromabdulmajeed-jamaan/filament-translatable-tabs:
Single field vs multiple fields
Single field
One template field → one input per locale tab. Ideal for titles, names, short strings.Multiple fields (group)
Several templates share the same locale tabs — all fields in a tab belong to that locale.Locales configuration
Locales can be supplied inline, split across codes and labels, or read from config.Inline map (locale => label)
List of codes + separate labels
en → EN).
From config (omit ->locales())
translatable.locales is unset, the resolver falls back to slug.translatable_locales.
Both locales() and localesLabels() accept Closure for dynamic resolution (e.g. tenant-specific languages).
Production presets
Bundled helpers for common production UX. Combine individually or use the bundle.withRecommendedDefaults(?string $emptyBadgeLabel = null)
Applies all three presets below. Pass a custom empty-badge label or rely on config (translatable.empty_badge_label, default 'empty').
borderedPanels(bool $condition = true)
Adds class fff-translatable-fields--bordered so the active tab panel renders inside a bordered card (rounded-xl, padding). Off by default — fields sit flush under the locale tabs. Use when you want a contained content area (e.g. multi-field groups in a Section).
directionByLocale(TranslatableDirectionScope $scope = TranslatableDirectionScope::Auto)
Sets dir="rtl" or dir="ltr" per locale tab. Default list from config:
ar-SA or he_IL match by primary language subtag (BCP 47).
By default (Auto), direction is applied to the editable control (extraInputAttributes()) when the field supports Filament’s HasExtraInputAttributes — labels and helper text keep the panel direction. Fields without a native input (custom layouts) still receive dir on the field wrapper so nothing breaks.
emptyBadgeWhenAllFieldsAreEmpty(?string $emptyLabel = null)
Shows a warning badge on locale tabs where all schema fields are empty. Useful on edit forms to spot untranslated locales.
->live() so badges update as the user types.
activeTabWithValue()
On mount, selects the first locale tab that has at least one non-empty field. Falls back to tab 1 when all tabs are empty.
Storage — JSON / array (no Spatie)
Recommended minimum setup. Works out of the box with Filament’s nested state merging.
On edit,
TranslatableHydrator reads the JSON/array attribute and fills each locale field when its state is empty.
Storage — Spatie laravel-translatable (optional)
Works alongside array/json casts when Spatie is not used on the model.
Advanced customization
localeFieldUsing(Closure $callback)
Replace the default field-cloning strategy. Return a custom Field instance or null to fall back to the default clone.
$template, $locale, $tab.
storageAttributeUsing(Closure $callback)
Override the Eloquent attribute used for hydration (default: template field name).
modifyTabsUsing(Closure $closure, bool $merge = true)
Run callbacks against each TranslatableTab after build. $merge = false replaces all previous tab modifiers.
modifyFieldsUsing(Closure $closure, bool $merge = true)
Run callbacks against each cloned field. $merge = false replaces all previous field modifiers.
directionByLocale() and emptyBadgeWhenAllFieldsAreEmpty() are implemented as stacked modifyTabsUsing / modifyFieldsUsing callbacks.
Custom tab badges
Beyond the empty-badge preset,TranslatableTab (extends SegmentTab) supports Filament badge APIs:
State paths and nested attributes
TranslatableAttributePath resolves form paths from template field names and optional custom statePath():
Storage attribute for hydration defaults to the field
name (title), not the full state path.
Custom configuration API
schema(array|Closure $fields)
One or more Field instances used as templates. Only Field subclasses are supported — other schema components will throw at build time.
locales(array|Closure $locales)
Locale codes as a list (['ar', 'en']) or map (['ar' => 'Arabic', 'en' => 'English']).
localesLabels(array|Closure $localeLabels)
Labels keyed by locale code. Used when locales() is a plain list.
spatieTranslatable(bool|Closure $condition = true)
Marks fields for Spatie-aware dehydration and documents intent. Hydration auto-detects HasTranslations on the record when the package is installed.
localeFieldUsing(Closure $callback) / storageAttributeUsing(Closure $callback)
See Advanced customization.
modifyTabsUsing(Closure $closure, bool $merge = true) / modifyFieldsUsing(Closure $closure, bool $merge = true)
See Advanced customization.
directionByLocale(TranslatableDirectionScope $scope = Auto) / emptyBadgeWhenAllFieldsAreEmpty(?string $emptyLabel = null) / activeTabWithValue() / withRecommendedDefaults(?string $emptyBadgeLabel = null) / borderedPanels(bool $condition = true)
See Production presets.
Inherited SegmentTabs API
TranslatableFields extends SegmentTabs. Defaults differ: separators are off (separators(false) in setUp()), CSS class fff-translatable-fields is applied, and tab panels are flat (no border/padding). Use borderedPanels() for a card-style panel. Multiple fields in one tab get vertical spacing only (mt-4 between field wrappers).
Global defaults
TranslatableFields::configureUsing()
Filament-native global defaults (same pattern as other Filament components):
boot() method. Every TranslatableFields::make() (including macro-created instances) receives these defaults.
Config file
Architecture overview
Internal services keep the component thin and testable:
Component concerns (under
TranslatableFields/Concerns/):
Relationship to TitleSlugField
For translatable titles with permalink generation, see Translatable titles (single slug).
Playground
Slug:translatable-fields — /admin/flex-fields-playground/translatable-fields