Summary
Locale-aware schema layout built on SegmentTabs (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()/binding configuration.
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.
Before native Filament hydration, the locale owner prepares JSON/array maps. Explicit empty maps,
null and cleared locale values remain authoritative. Rendering and badge evaluation never reload a record value over an edit.
Storage — Spatie laravel-translatable (optional)
Spatie remains optional and is not added to the package’s required dependencies.
The editor reads stored locale maps; application-facing fallback and translation
query APIs remain the model’s responsibility. See the Spatie v6 documentation.
Advanced customization
localeFieldUsing(Closure $callback, bool $useTemplateStatePath = false)
Replace the default field-cloning strategy. Return a custom Field instance or null to fall back to the default clone. Custom instances are cloned for isolation; their own names/paths remain intact by default for compatibility. In that mode the callback owns state mapping and persistence.
Pass useTemplateStatePath: true to replace only the renderer: FLEX assigns the
template’s automatic locale path and storage binding. This is the recommended mode
when the replacement should participate in standard JSON/Spatie persistence.
$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.
borderedPanels(false) reverses a previous true; it also accepts an injected closure.
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, bool $useTemplateStatePath = false) / 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/translatable-fields
Related components
Part III — Appendix
Shared locale tabs
Locale tabs reuse SegmentTabs and its Control renderer, CSS and runtime. Each locale schema is cloned and modified once per instance. Changing templates/locales/modifiers invalidates that cache; cloning the wrapper creates an independent cache. Actual child fields declare their own assets, so the wrapper does not preload every possible input type. Multiple locale groups have independent selection and unique panel IDs, including inside nested Filament action modals.Locale keys and lifecycle contract
Locale keys retain their exact spelling, includingpt_BR and zh-Hant, so they
match existing Spatie data. Duplicate keys and invalid dot-path codes are rejected;
localesLabels() can override labels for both lists and locale-to-label maps.
Schemas and injected locale/storage callbacks resolve in their actual form context.
Locale templates are isolated clones. The owner caches one composed schema per
instance, clears it after configuration changes or cloning, and discards a partly
configured cache when a modifier fails. Defaults and all native callbacks run through
Filament; the translation adapter never replaces them. Full and partial fills use
the same preparation rules, including inside repeater items.
The wrapper shares SegmentTabs’ renderer and runtime. Each actual child retains
its own CSS/JS consumer boundary; configuring many locales does not eagerly activate
fields in inactive tabs. Code examples remain separate files, read only by the
catalog’s Code Example surface.
Behavior changes in the rebuild: locale keys are no longer lowercased, explicit
Spatie opt-in validates its optional dependency/model contract, and strings are no
longer trimmed or converted to null by FLEX. Applications requiring that conversion
can declare it explicitly with the field’s dehydrateStateUsing() callback.