laravel-tipi / translations
Flexible Eloquent model translations for Laravel with dedicated-table, shared-table, and JSON storage.
Requires
- php: ^8.5
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- laravel-tipi/localization: ^0.1
- laravel-tipi/support: ^1.0
Requires (Dev)
- laravel/pint: ^1.32
- orchestra/testbench: ^11.3
- pestphp/pest: ^5.3
- pestphp/pest-plugin-laravel: ^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Flexible Eloquent model translations for Laravel.
The package lets each translatable model choose the storage strategy that fits it:
- Dedicated table — one translation table per model.
- Shared table — one polymorphic translations table for many model types.
- JSON columns — translations stored directly on the model.
It integrates with laravel-tipi/localization for supported, current, and default locales.
Requirements
- PHP 8.5+
- Laravel 13
laravel-tipi/localization ^0.1
Installation
Install the package with Composer:
composer require laravel-tipi/translations
Laravel discovers Tipi\Translations\TranslationServiceProvider automatically.
The package migration creates the shared translations table. Run your application migrations:
php artisan migrate
To publish the package configuration:
php artisan vendor:publish --tag=translation-config
Configuration
config/translation.php controls package-wide infrastructure:
return [ 'translations_table' => 'translations', 'translation_model' => \Tipi\Translations\Models\TranslationModel::class, 'locale_provider' => \Tipi\Translations\Providers\LocalizationLocaleProvider::class, ];
Storage strategy is selected by each model through its contract and trait; it is not a global driver setting.
Dedicated translation table
Use this strategy when a model should have its own normalized translation table.
use Illuminate\Database\Eloquent\Model; use Tipi\Translations\Concerns\HasDedicatedTableTranslations; use Tipi\Translations\Contracts\DedicatedTableTranslatableModel; final class Article extends Model implements DedicatedTableTranslatableModel { use HasDedicatedTableTranslations; protected static array $translatableAttributes = [ 'title', 'description', ]; }
By convention the package resolves ArticleTranslation. The translation model uses IsTranslation:
use Illuminate\Database\Eloquent\Model; use Tipi\Translations\Concerns\IsTranslation; use Tipi\Translations\Contracts\TranslationModelContract; final class ArticleTranslation extends Model implements TranslationModelContract { use IsTranslation; }
Your translation table should contain the parent foreign key, locale_code, translated columns, nullable outdated_at, and timestamps. Add a unique constraint for the parent foreign key plus locale_code.
Shared translation table
Use the shared polymorphic table when many models can use the same translation schema.
use Illuminate\Database\Eloquent\Model; use Tipi\Translations\Concerns\HasSharedTableTranslations; use Tipi\Translations\Contracts\SharedTableTranslatableModel; final class Article extends Model implements SharedTableTranslatableModel { use HasSharedTableTranslations; protected static array $translatableAttributes = [ 'title', 'description', ]; }
The package migration stores one row per model and locale. Translated values are stored in the row's values JSON column.
JSON translations
Use JSON storage when translated values should live directly on the model table.
use Illuminate\Database\Eloquent\Model; use Tipi\Translations\Concerns\HasJsonTranslations; use Tipi\Translations\Contracts\JsonTranslatableModel; final class Article extends Model implements JsonTranslatableModel { use HasJsonTranslations; protected static array $translatableAttributes = [ 'title', 'description', ]; }
Each translatable attribute must be backed by a JSON column.
JSON translations intentionally do not support outdated-translation tracking.
Reading translations
Translatable attributes resolve using the current locale:
$article->title;
You can explicitly choose a locale or provide a fallback value:
$article->translated( attribute: 'title', default: 'Untitled', localeCode: 'en', );
Creating translations
Use the package actions for writes:
use Tipi\Translations\Actions\CreateTranslation; $translation = resolve(CreateTranslation::class)->execute( translatable: $article, attributes: [ 'title' => 'ქართული სათაური', 'description' => 'ქართული აღწერა', ], localeCode: 'ka', );
If localeCode is omitted when creating a translation, the default locale is used.
Updating translations
use Tipi\Translations\Actions\UpdateTranslation; $translation = resolve(UpdateTranslation::class)->execute( translatable: $article, attributes: [ 'title' => 'Updated title', ], localeCode: 'en', );
Updates are partial: omitted attributes remain unchanged, while an explicitly supplied null clears that translated value.
For table-backed strategies, updating the default translation can mark the other translations as outdated:
resolve(UpdateTranslation::class)->execute( translatable: $article, attributes: ['title' => 'Updated default title'], localeCode: 'ka', markOthersAsOutdated: true, );
Deleting translations
use Tipi\Translations\Actions\DeleteTranslation; resolve(DeleteTranslation::class)->execute( translatable: $article, localeCode: 'en', );
The default translation cannot be deleted independently.
Hard-deleting a table-backed translatable model deletes its translation records. Soft-deleting the parent keeps them; force-deleting removes them.
Transactions
Create, update, and delete actions use a database transaction by default and lock the translatable row before writing.
If you already own the surrounding transaction, you can disable the action's transaction wrapper:
resolve(UpdateTranslation::class)->execute( translatable: $article, attributes: ['title' => 'Updated title'], dbTransaction: false, );
Development
Run the complete package check:
composer check
Or run the tools separately:
composer test
composer format:test
License
Laravel Tipi Translations is open-source software licensed under the MIT License.