Translatable Master Data
Some texts are not yours but your customer's: an item's name, a product's description, the title of a service. A company in Zurich keeps them in German and adds French and Italian. The SDK carries these further languages for any model you mark.
Provisional
This part may still change in a minor release, with notice in the changelog. It becomes stable with the first multilingual installation in production.
Mark the model
use LaravelUi5\Sdk\I18n\Attributes\Translatable;
use LaravelUi5\Sdk\I18n\Concerns\HasTranslations;
#[Translatable(['title', 'description'])]
final class Item extends Model
{
use HasTranslations;
}The attribute declares the fields; the trait does the work. Nothing else is registered.
Where the texts live
The base language stays in the model's own column, items.title, so sorting and filtering in the base language are plain SQL. The further languages live in one table of the SDK, sdk_translations, keyed by table, row, field and language. When the row is deleted, its translations go with it.
Reading
Every query of the model delivers the marked fields in the request's language: the translation where one exists, else the base value of the column, never English. $filter, $orderby and $search run on that text. The field itself carries it, so an OData set behind discoverModel needs nothing extra.
In code, two methods read a single field:
$item->translated('title', 'fr'); // the French text, else the base value
$item->translation('title', 'fr'); // the stored French text, or nullWriting
Translations are written from the owning module's action, inside its transaction:
$item->setTranslations([
'fr' => ['title' => 'Vis'],
'it' => ['title' => 'Vite'],
]);An empty text removes a translation. The writer refuses the base language (that is the column's), a language that is not one of the further languages, and a field the attribute does not name. For the action's FormRequest, TranslationWriter's rules(Item::class) returns one optional rule per further language and field (translations.fr.title).
Which languages
LanguagesInterface answers two questions: the base language, and the further languages. The SDK's default answers with the app's locale and no further language, so out of the box master data is written in its base language only. A host that knows the company's languages binds its own:
use LaravelUi5\Sdk\I18n\Contracts\LanguagesInterface;
$this->app->bind(LanguagesInterface::class, CompanyLanguages::class);Codes are ISO 639-1, lower case: de, fr, the language part the request carries.
The edit dialog
An edit form binds the translations navigation, not the field: the field arrives already in the reader's language, while translations lists every language, the base included, so the form can show one input per language. A module gives its model the navigation in a few lines and registers it with discoverCustomEntitySet():
use LaravelUi5\Sdk\I18n\OData\AbstractTranslationsExpand;
final class ItemTranslations extends AbstractTranslationsExpand
{
protected function model(): string { return Item::class; }
protected function parentEntityType(): string { return 'Item'; }
}