Building multilingual runtime collections in a Filament plugin
I just released v1.2.0 of Filament Studio, a Filament v5 plugin I am building for runtime collections, custom fields, dashboards, filters, and APIs.
The short version: this release adds multilingual support.
The longer version: adding multilingual support to a runtime data model is not the same as adding a translations JSON column and moving on.
Filament Studio is built for admin panels where the data model changes after the app is already running. A user can create a collection, add fields, manage records, build dashboards, and expose API endpoints without writing a new migration and Filament resource for each new content type.
That flexibility creates a specific problem for translations.
If the fields are defined at runtime, translation behavior also has to be defined at runtime.
The problem I wanted to solve
In real admin systems, multilingual content is rarely all-or-nothing.
A collection might support English and French.
Inside that collection:
-
titleshould be translated -
descriptionshould be translated -
priceshould stay shared -
published_atshould stay shared -
is_featuredshould stay shared
That means translation behavior belongs at the field level, not only at the record or collection level.
I also wanted the feature to work across the whole plugin:
- generated Filament forms
- EAV storage
- query builder reads and writes
- REST API responses
- OpenAPI documentation
- fallback behavior
- version snapshots
Otherwise it would feel like a partial feature.
Global and collection config
Multilingual support is opt-in.
// config/filament-studio.php
'locales' => [
'enabled' => true,
'available' => ['en', 'fr', 'de'],
'default' => 'en',
],
You can also enable it through the environment:
STUDIO_LOCALES_ENABLED=true
Each collection can then define its own supported locales and default locale:
$collection->update([
'supported_locales' => ['en', 'fr'],
'default_locale' => 'en',
]);
If a collection does not define supported locales, it uses the global list.
Per-field translation
Each field now has a translatable flag.
That lets a collection mix localized and shared data:
title translatable
description translatable
price shared
published_at shared
is_featured shared
This is the part of the design I care about most. In multilingual admin work, not every value is content. Some values are facts, flags, prices, dates, or relationships. Treating all of them as translatable creates noise and sometimes bugs.
Storage model
Filament Studio uses an EAV model because fields are created at runtime.
For v1.2.0, the studio_values table now includes a locale column.
For translatable fields, values are stored per locale.
For shared fields, one value is used regardless of active locale.
The uniqueness boundary is:
record_id + field_id + locale
That allows a record to have different localized values for the same runtime field without creating new database columns for every translated field.
Locale resolution
I added a dedicated LocaleResolver service so the rules live in one place.
The active locale is resolved in this order:
-
?locale=query parameter -
X-Localeheader - session
- collection default
- global default
That gives API consumers a direct way to request a locale, while the admin panel can remember the selected locale in the session.
Query builder API
The EavQueryBuilder now supports a locale() method:
$data = EavQueryBuilder::for($collection)
->locale('fr')
->getRecordData($record);
Create and update operations can also target a locale:
$record = EavQueryBuilder::for($collection)
->locale('fr')
->create([
'title' => 'Mon Titre',
]);
EavQueryBuilder::for($collection)
->locale('fr')
->update($record->id, [
'title' => 'Titre mis a jour',
]);
There is also getAllLocaleData() for retrieving every translation at once:
$allData = EavQueryBuilder::for($collection)
->getAllLocaleData($record);
Example result:
[
'title' => [
'en' => 'My Title',
'fr' => 'Mon Titre',
],
'price' => 29.99,
]
Translatable fields return locale maps. Shared fields stay plain.
Fallback metadata
I did not want fallback behavior to be silent.
If an API client asks for French and a field falls back to English, the client should be able to detect that.
So getRecordDataWithMeta() returns fallback information:
$result = EavQueryBuilder::for($collection)
->locale('fr')
->getRecordDataWithMeta($record);
Example:
[
'data' => [
'title' => 'Mon Titre',
'slug' => 'my-slug',
],
'fallbacks' => ['slug'],
]
That tells the caller that slug came from the default locale.
REST API support
API endpoints now accept locale selection through query params:
curl -H "X-Api-Key: your-key" \
"https://your-app.com/api/studio/posts?locale=fr"
or through headers:
curl -H "X-Api-Key: your-key" \
-H "X-Locale: fr" \
"https://your-app.com/api/studio/posts"
Responses include metadata:
{
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"title": "Mon Titre",
"slug": "my-slug",
"price": 29.99
}
},
"_meta": {
"locale": "fr",
"fallbacks": ["slug"]
}
}
For single-record reads, all_locales=true returns every locale:
curl -H "X-Api-Key: your-key" \
"https://your-app.com/api/studio/posts/550e8400?all_locales=true"
OpenAPI docs
When multilingual support is enabled, the generated OpenAPI docs include:
-
localequery parameter -
X-Localeheader parameter -
all_localesquery parameter _meta.locale_meta.fallbacks
This felt worth doing because API features are easy to misunderstand when the docs do not show the request and response shape.
Admin UI and versioning
In the Filament admin, multilingual collections now get a locale switcher on record pages.
Editors can switch locale and edit only the fields that should change for that locale.
Version snapshots also store all locale values for translatable fields:
{
"title": {
"en": "My Title",
"fr": "Mon Titre"
},
"slug": {
"en": "my-slug"
},
"price": 29.99
}
That way restoring a version does not only restore whichever locale happened to be active at the time.
Why I am sharing this
This was one of those updates that made the package feel more practical.
The first version of a dynamic admin builder can look good with just runtime fields and generated forms.
But real projects keep asking for the less glamorous pieces:
- permissions
- APIs
- versioning
- tenancy
- filtering
- multilingual content
That is the direction I am trying to push Filament Studio.
GitHub: https://github.com/flexpik/filament-studio
Packagist: https://packagist.org/packages/flexpik/filament-studio
If you have built multilingual admin panels in Laravel, I would like feedback on the model:
- should translation behavior live per field?
- is fallback metadata useful in practice?
- would
all_locales=truework for your API use cases? - what edge cases am I missing?
Top comments (0)