Skip to main content
TranslatableFields ← Back to Table of Contents

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 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.
Tabs use ->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.
State shape: 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.
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 such as 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):
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-playground/translatable-fields

Part III — Appendix