Skip to main content
← Back to Table of Contents

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 Spatie laravel-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.
On save, Filament merges 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 declaring TranslatableFields explicitly. The macro returns the TranslatableFields component (replace the field in your schema with the return value).
The macro uses the field’s label as the segment-tabs heading and wraps the field as the sole template.

Macro with inline modifiers

Legacy aliases

For projects migrating from abdulmajeed-jamaan/filament-translatable-tabs:
Third-party preset method names are also aliased on the component:

Single field vs multiple fields

Single field

One template field → one input per locale tab. Ideal for titles, names, short strings.
Or via macro:

Multiple fields (group)

Several templates share the same locale tabs — all fields in a tab belong to that locale.
Empty-badge logic considers all fields in a tab: the badge appears only when every field in that locale tab is empty.

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

When only codes are given and no matching label exists, the tab label defaults to the uppercased locale code (en → EN).

From config (omit ->locales())

If 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:
Locales such as 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.
This preset enables inherited live binding with a 250 ms debounce so badges update as the user types. A template can override that policy with its own 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.
State shape: 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.
Injected closure parameters: $template, $locale, $tab.

storageAttributeUsing(Closure $callback)

Override the Eloquent attribute used for hydration (default: template field name).
Useful when the form field name differs from the database column, or when multiple templates map to custom storage logic.

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.
Presets are typed options applied once to each locale clone before user modifiers. Repeating a preset replaces its option instead of stacking callbacks. 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):
Register in a service provider 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

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, including pt_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.
Last modified on October 24, 2026