Translatable Fields
FormBuilder's translatable field types let you store and edit multi-language text in a single JSON database column. Active languages are driven by the general.languages setting; adding or removing a language in Settings updates every translatable form on the next page load.
What It Does
- Renders one input per active language, grouped with a locale badge (
TR,EN, ...). - Stores values as
{"tr": "...", "en": "..."}in JSON columns throughspatie/laravel-translatable. - Generates per-locale Laravel validation rules from one FormRequest trait call.
- Provides locale-aware search, sort and resource helpers for datatable responses.
What It Does Not Do
- It does not migrate existing plain string columns to JSON automatically.
- It does not generate per-locale slugs.
- It does not support translatable select, enum or checkbox values.
- It does not change Spatie Translatable's fallback behaviour.
- It does not provide bulk translation import/export or automatic translation tooling.
Quick Start
Store translated attributes in JSON columns:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->json('name');
$table->json('description')->nullable();
$table->timestamps();
});
Use Spatie's HasTranslations trait on the model:
use Spatie\Translatable\HasTranslations;
class Product extends Model
{
use HasTranslations;
public array $translatable = ['name', 'description'];
protected $fillable = ['name', 'description'];
}
Generate validation rules with HasTranslatableRules:
use Lvntr\StarterKit\Support\HasTranslatableRules;
class StoreProductRequest extends FormRequest
{
use HasTranslatableRules;
public function rules(): array
{
return [
...$this->translatableRules('name', ['required', 'string', 'max:255']),
...$this->translatableRules('description', ['nullable', 'string']),
];
}
public function attributes(): array
{
return $this->translatableAttributes([
'name' => __('product.name'),
'description' => __('product.description'),
]);
}
}
Render fields with FormBuilder:
FB.form().addFields(
FB.translatableText().key('name').label('Product Name').required(),
FB.translatableTextarea().key('description').label('Description').rows(4),
FB.translatableEditor().key('content').label('Content'),
);
Return translation-aware values from resources:
use Lvntr\StarterKit\Support\TranslatableQueryHelpers;
'name' => TranslatableQueryHelpers::resourceShape($this->resource, 'name'),
// ['translations' => ['tr' => '...', 'en' => '...'], 'current' => '...']
Field Builders
FB.translatableText()
Renders a PrimeVue InputText per locale.
| Method | Type | Description |
|---|---|---|
.placeholder(value) |
string |
Placeholder for every locale input. |
.inputType(value) |
'text' | 'email' | 'url' |
HTML input type. |
.maxLength(value) |
number |
maxlength attribute on each input. |
.onlyLocales(locales) |
string[] |
Render only the listed locale codes. |
.exceptLocales(locales) |
string[] |
Hide the listed locale codes. |
.localeLabelStyle(style) |
'badge' | 'name' | 'flag' |
Locale label style. |
FB.translatableTextarea()
Renders a PrimeVue Textarea per locale.
| Method | Type | Description |
|---|---|---|
.placeholder(value) |
string |
Placeholder for every locale textarea. |
.rows(value) |
number |
Visible row count. |
.autoResize(enabled = true) |
boolean |
Enables PrimeVue auto-resize. |
It also supports .onlyLocales(), .exceptLocales() and .localeLabelStyle().
FB.translatableEditor()
Renders the kit editor per locale.
| Method | Type | Description |
|---|---|---|
.minHeight(value) |
string |
Editor minimum height, for example '220px'. |
.toolbar(value) |
'minimal' | 'full' |
Toolbar preset. |
It also supports .onlyLocales(), .exceptLocales() and .localeLabelStyle().
Validation Helpers
HasTranslatableRules is a trait that runs from vendor (Lvntr\StarterKit\Support\HasTranslatableRules) since v13.5.12, intended for Laravel FormRequests. Import it directly from the vendor namespace. PHP traits cannot be aliased like classes, so there is no App\Support\HasTranslatableRules fallback — if a project upgraded from an older version still has a local copy at app/Support/HasTranslatableRules.php, update the use statement to the vendor namespace before deleting that copy.
$this->translatableRules('title', ['required', 'string', 'max:255']);
With tr and en active, the primary locale keeps required; optional locales replace required with nullable:
[
'title.tr' => ['required', 'string', 'max:255'],
'title.en' => ['nullable', 'string', 'max:255'],
]
Options:
| Option | Type | Description |
|---|---|---|
primary |
array |
Override rules for the primary locale. |
optional |
array |
Override rules for non-primary locales. |
only |
list<string> |
Apply rules only to these locales. |
except |
list<string> |
Skip these locales. |
Use translatableAttributes() for readable validation labels:
$this->translatableAttributes(['title' => __('articles.fields.title')]);
// title.tr => Title (Turkish), title.en => Title (English)
Query Helpers
TranslatableQueryHelpers runs from vendor (Lvntr\StarterKit\Support\TranslatableQueryHelpers) since v13.5.12. The App\Support\TranslatableQueryHelpers import keeps working transparently via a class_alias, so existing code is unaffected.
return DatatableQueryBuilder::for(Product::class)
->filterable([
TranslatableQueryHelpers::searchFilter('name'),
])
->sortable([
TranslatableQueryHelpers::localeSort('name'),
'created_at',
])
->response();
searchFilter('name')searches all active locales withLIKE.searchFilter('name', 'tr')searches only one locale.localeSort('name')sorts byapp()->getLocale().localeSort('name', 'en')sorts by a fixed locale.resourceShape($model, 'name')returnstranslationsplus the current locale value.
Settings Interaction
Translatable fields read active languages from Settings -> General (general.languages). When a user updates Settings, the Settings cache is cleared automatically.
Adding a language shows a new locale input on the next render. Existing records keep their old JSON shape until you backfill the new locale manually.
Removing a language hides that locale in forms, but it does not delete the stored JSON key. Re-adding the language makes the stored value visible again.
Single-language mode still stores JSON, for example {"tr": "..."}. This keeps future multi-language expansion migration-free.
Migration Tips
Do not convert a busy plain string column to JSON in a single destructive migration. Use a two-step deployment:
- Add a new nullable JSON column and backfill old values into the default locale.
- Deploy code that reads the new column.
- Rename/drop the old column in a later release after verifying data.
Example backfill:
Schema::table('products', function (Blueprint $table) {
$table->json('name_translatable')->nullable()->after('name');
});
DB::table('products')->orderBy('id')->chunk(200, function ($rows) {
foreach ($rows as $row) {
DB::table('products')->where('id', $row->id)->update([
'name_translatable' => json_encode([sk_default_locale() => $row->name]),
]);
}
});
Notes
- Translatable fields do not support
.groupPrefix()or.groupSuffix()because each locale already renders its own input group. - Use
.onlyLocales(['tr'])or.exceptLocales(['en'])only for fields whose business meaning is language-specific. - For rich HTML, keep the same sanitizer discipline used by
FB.editor(): sanitize on write and again before rendering withv-html.