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 through spatie/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 with LIKE.
  • searchFilter('name', 'tr') searches only one locale.
  • localeSort('name') sorts by app()->getLocale().
  • localeSort('name', 'en') sorts by a fixed locale.
  • resourceShape($model, 'name') returns translations plus 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:

  1. Add a new nullable JSON column and backfill old values into the default locale.
  2. Deploy code that reads the new column.
  3. 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 with v-html.