Filament Studio supports opt-in multilingual content for translatable fields. When enabled, each field can store different values per locale, with automatic fallback to the default locale when a translation is missing.
Enable multilingual globally via config:
// config/filament-studio.php
'locales' => [
'enabled' => true,
'available' => ['en', 'fr', 'de'],
'default' => 'en',
],
Or via environment variable:
STUDIO_LOCALES_ENABLED=true
Each collection can define its own subset of available locales and a default locale. Configure this in the collection settings under the "Multilingual" section (only visible when locales.enabled is true).
$collection->update([
'supported_locales' => ['en', 'fr'],
'default_locale' => 'en',
]);
If a collection has no supported_locales set, it inherits the global locales.available list.
Individual fields are marked as translatable via the "Translatable" toggle in the field editor's behavior settings. Only fields where per-locale content makes sense should be translatable — for example, a "Title" or "Description" field, but not a "Price" or "Date" field.
When a field is translatable:
studio_values with a locale column[record_id, field_id, locale] prevents duplicate entriesThe LocaleResolver service determines the active locale using a priority chain:
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | ?locale= query parameter |
?locale=fr |
| 2 | X-Locale request header |
X-Locale: fr |
| 3 | Session value | studio_locale session key |
| 4 | Collection default | $collection->default_locale |
| 5 (lowest) | Global default | config('filament-studio.locales.default') |
The first valid locale found in this chain is used. A locale is considered valid if it appears in the collection's supported_locales (or the global available list).
When a translatable field has no value for the requested locale, it falls back to the default locale. The EavQueryBuilder::getRecordDataWithMeta() method returns metadata about which fields fell back:
$result = EavQueryBuilder::for($collection)
->locale('fr')
->getRecordDataWithMeta($record);
// $result['data'] => ['title' => 'Mon Titre', 'slug' => 'my-slug']
// $result['fallbacks'] => ['slug'] — slug fell back to the default locale
When multilingual is enabled and a collection has multiple locales, a locale switcher appears in the record edit and view pages. Clicking a locale button switches the form to display values for that locale. The selected locale persists in the session.
The version history slide-over includes a locale switcher at the top. Translatable fields display the value for the selected locale with a locale badge (e.g. EN), and diffs compare values within the same locale. Non-translatable fields display normally regardless of the selected locale.
The query builder accepts a locale() fluent method that applies to all subsequent operations:
// Read data in a specific locale
$data = EavQueryBuilder::for($collection)
->locale('fr')
->getRecordData($record);
// Create a record in a specific locale
$record = EavQueryBuilder::for($collection)
->locale('fr')
->create(['title' => 'Mon Titre']);
// Update a record in a specific locale
EavQueryBuilder::for($collection)
->locale('fr')
->update($record->id, ['title' => 'Titre Mis a Jour']);
// Get all locale data at once
$allData = EavQueryBuilder::for($collection)
->getAllLocaleData($record);
// => ['title' => ['en' => 'My Title', 'fr' => 'Mon Titre'], 'price' => 29.99]
The getAllLocaleData() method returns translatable fields as nested locale maps and non-translatable fields as plain values.
All API endpoints support locale selection. See the REST API documentation for details.
curl -H "X-Api-Key: your-key" \
"https://your-app.com/api/studio/posts?locale=fr"
curl -H "X-Api-Key: your-key" \
-H "X-Locale: fr" \
"https://your-app.com/api/studio/posts"
Responses include _meta with the active locale and any fields that fell back:
{
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"title": "Mon Titre",
"slug": "my-slug",
"price": 29.99
},
"created_by": 1,
"updated_by": null,
"created_at": "2025-01-15T10:30:00.000000Z",
"updated_at": "2025-01-15T10:30:00.000000Z"
},
"_meta": {
"locale": "fr",
"fallbacks": ["slug"]
}
}
Use ?all_locales=true to get all translations in a single response:
curl -H "X-Api-Key: your-key" \
"https://your-app.com/api/studio/posts/550e8400?all_locales=true"
{
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"title": {"en": "My Title", "fr": "Mon Titre"},
"slug": {"en": "my-slug", "fr": "my-slug"},
"price": 29.99
},
"created_by": 1,
"updated_by": null,
"created_at": "2025-01-15T10:30:00.000000Z",
"updated_at": "2025-01-15T10:30:00.000000Z"
}
}
Translatable fields appear as nested locale objects. Non-translatable fields remain plain values.
When multilingual is enabled and Scramble is installed, the auto-generated API documentation includes:
locale query parameter with an enum dropdown of available localesX-Locale header parameter as an alternative locale selectorall_locales boolean query parameter on GET single-record endpoints_meta object in response schemas with locale and fallbacks fieldsField metadata (labels, placeholders, hints) can also be translated per locale using the translations JSON column on StudioField:
$field->update([
'translations' => [
'fr' => ['label' => 'Titre', 'placeholder' => 'Entrez un titre'],
'de' => ['label' => 'Titel', 'placeholder' => 'Titel eingeben'],
],
]);
// Resolve label for active locale
$label = $field->getTranslatedAttribute('label'); // "Titre" when locale is fr
If no translation exists for the active locale, the base attribute value is used.
When versioning is enabled, snapshots capture all locale values for translatable fields:
{
"title": {"en": "My Title", "fr": "Mon Titre"},
"slug": {"en": "my-slug"},
"price": 29.99
}
Restoring a version writes back all locale rows for translatable fields.
How can I help you explore Laravel packages today?