Translatable
Learn how to translate individual model attributes across locales.
For an overview of when to use this trait versus Multisite or MultisiteGroup, see the Introduction.
Translatable models store translated attribute values in a separate table, with one row per model, per attribute, per locale. The active locale is resolved automatically from the Site facade. To make attributes translatable, apply the October\Rain\Database\Traits\Translatable trait and declare a $translatable property with an array containing the attributes to translate.
class Product extends Model
{
use \October\Rain\Database\Traits\Translatable;
public $translatable = ['name', 'description', 'slug'];
}
The trait automatically registers a translations morphMany relationship and stores translations in the system_translate_attributes table.
# How It Works
When the active site locale differs from the default locale, the trait intercepts getAttribute and setAttribute to read and write translated values instead of model attributes. When only one locale exists (or the default locale is active), the trait is invisible — no extra queries, no relationship loading, just a single string comparison on every attribute access.
Translated values are stored in the system_translate_attributes table:
| model_type | model_id | locale | attribute | value |
|------------|----------|--------|-----------|---------|
| Product | 42 | fr | name | Produit |
| Product | 42 | fr | slug | produit |
| Product | 42 | de | name | Produkt |
# Reading Translations
Translated values are returned automatically based on the active site locale when accessing model attributes.
$product->name; // Returns translated value or falls back to default
Use the getTranslation method to read a translation for a specific locale.
$product->getTranslation('name', 'fr');
By default, if no translation exists for the requested locale, the method falls back to the default locale value. To disable fallback and return null instead, pass false as the third argument.
$product->getTranslation('name', 'fr', useFallback: false);
Use the getTranslations method to get all locale values for a single attribute.
$product->getTranslations('name');
// Returns: ['en' => 'Product', 'fr' => 'Produit', 'de' => 'Produkt']
# Writing Translations
Translations are written automatically when the active locale is set and the model is saved.
$product->setLocale('fr');
$product->name = 'Produit';
$product->save();
Use the setTranslation method to set a translation for a specific locale. The model must be saved afterward.
$product->setTranslation('name', 'fr', 'Produit');
$product->save();
Use the setTranslations method to set multiple locales at once for a single attribute.
$product->setTranslations('name', [
'fr' => 'Produit',
'de' => 'Produkt',
]);
$product->save();
For non-default locales, attributes whose value matches the default locale value are not stored. They inherit from the model table via fallback, so changes to the default automatically propagate to untranslated locales.
# Deleting Translations
Use the forgetTranslation method to remove a single translation.
$product->forgetTranslation('name', 'fr');
Use the forgetTranslations method to remove all translations for an attribute across all locales.
$product->forgetTranslations('name');
Use the forgetAllTranslations method to remove all translations for a locale, effectively "unpublishing" that language.
$product->forgetAllTranslations('fr');
# Checking Translations
Use the hasTranslation method to check if a specific attribute has been translated.
$product->hasTranslation('name', 'fr'); // true or false
Use the hasTranslations method to check if the model has any translations at all for a given locale.
$product->hasTranslations('fr'); // true if ANY attribute has a translation for 'fr'
Use the getTranslatedLocales method to get a list of locales that have translations. Pass an attribute name to check locales for a specific attribute.
$product->getTranslatedLocales(); // ['fr', 'de']
$product->getTranslatedLocales('name'); // ['fr', 'de']
# Locale Context
Use the setLocale method to override the locale for a model instance. This affects how getAttribute and setAttribute behave.
$product->setLocale('fr');
$product->name; // Returns French translation
$product->getLocale(); // 'fr'
The setLocale method is chainable.
$product->setLocale('fr')->name;
# Query Scopes
Use the whereTranslation scope to filter by translated attribute values.
Product::whereTranslation('name', 'fr', 'Produit')->get();
The scope also supports a fourth argument for the comparison operator.
Product::whereTranslation('name', 'fr', '%Prod%', 'like')->get();
Use the orderByTranslation scope to sort by translated attribute values.
Product::orderByTranslation('name', 'fr', 'asc')->get();
# Base Value Fallback Scopes
The whereTranslation and orderByTranslation scopes match and sort against stored translation rows only, so records with no translation for the requested locale are excluded from a where and sort as NULL in an order. When you want untranslated records to fall back to their default-locale value instead, use the transWhere and transOrderBy scopes.
Use the transWhere scope to filter by either the base value or the translated value for the locale. The locale defaults to the active locale when omitted.
Product::transWhere('name', 'Produit', 'fr')->get();
Like whereTranslation, it accepts a comparison operator.
Product::transWhere('name', '%Prod%', 'fr', 'like')->get();
Use the transOrderBy scope to sort by the translated value, falling back to the base column value for untranslated records. The locale defaults to the active locale when omitted.
Product::transOrderBy('name', 'asc', 'fr')->get();
# Eager Loading
Use the withTranslation scope to eager load translations for a single locale, avoiding N+1 queries.
Product::withTranslation('fr')->get();
When called without arguments, it uses the current site locale.
Product::withTranslation()->get();
Use the withTranslations scope to eager load all translations for all locales.
Product::withTranslations()->get();
You can also use standard Eloquent eager loading with the translations relationship.
Product::with('translations')->get();
# JSON and Array Attributes
Attributes declared as $jsonable on the model are handled transparently. Array values are serialized to JSON when stored in the translation table and deserialized back to arrays when read.
class Product extends Model
{
use \October\Rain\Database\Traits\Translatable;
public $translatable = ['options'];
protected $jsonable = ['options'];
}
$product->setTranslation('options', 'fr', ['color' => 'rouge']);
$product->save();
$product->getTranslation('options', 'fr'); // ['color' => 'rouge']
# Method Reference
| Method | Description |
|---|---|
getTranslation($key, $locale, $useFallback = true) | Get translated value for attribute and locale. |
setTranslation($key, $locale, $value) | Set translated value for attribute and locale. |
getTranslations($key) | Get all locale values for an attribute. |
setTranslations($key, array $translations) | Set multiple locales at once. |
hasTranslation($key, $locale = null) | Check if a translation row exists for one attribute. |
hasTranslations($locale = null) | Check if any translation rows exist for the locale (record-level). |
getTranslatedLocales($key = null) | Get locales with translations. |
forgetTranslation($key, $locale) | Delete a single translation row. |
forgetTranslations($key) | Delete all translation rows for an attribute. |
forgetAllTranslations($locale) | Delete all translation rows for a locale. |
setLocale($locale) | Override the locale context for this instance. |
getLocale() | Get the active locale. |
isTranslatableAttribute($key) | Check if attribute should be translated right now. |
shouldTranslate() | Check if translation is active (context !== default). |
getTranslatableAttributes() | Get the $translatable attribute names. |
whereTranslation($key, $locale, $value, $operator = '=') | Filter by a stored translated value only. |
orderByTranslation($key, $locale, $direction = 'asc') | Sort by a stored translated value (untranslated rows sort as NULL). |
transWhere($key, $value, $locale = null, $operator = '=') | Filter by the base or translated value, defaulting to the active locale. |
transOrderBy($key, $direction = 'asc', $locale = null) | Sort by the translated value, falling back to the base value. |