mosamy/translatable

A Laravel package to make your Eloquent models translatable.

Maintainers

Package info

bitbucket.org/mohamedsamy_10/translatable

pkg:composer/mosamy/translatable

Statistics

Installs: 102

Dependents: 0

Suggesters: 0

2.0.0 2026-04-21 00:04 UTC

This package is auto-updated.

Last update: 2026-04-21 00:04:19 UTC


README

A small Laravel package to store and query translated model attributes using a polymorphic translations table.

Features

  • Morph-many translation storage for any Eloquent model.
  • Automatic attribute translation fallback when the base model value is null.
  • Model-level control of which attributes are translatable.
  • Search, sorting, and filter scopes for translated values.
  • Built-in uniqueness validation rule for translation fields.
  • Automatic translation cleanup when the parent model is deleted.

Requirements

  • PHP 8.1+
  • Laravel application (service provider is auto-discovered)

Installation

composer require mosamy/translatable

Run migrations:

php artisan migrate

Database Structure

The package migration creates a translations table with:

  • id
  • locale
  • attribute
  • body
  • translatable_type
  • translatable_id

Unique index name: translation_unique

Unique columns:

  • translatable_type
  • translatable_id
  • locale
  • attribute

Model Setup

1) Add the trait

use Illuminate\Database\Eloquent\Model;
use Mosamy\Translatable\Translatable;

class Post extends Model
{
  use Translatable;
}

2) Configure translatable attributes (recommended)

Use the #[Translatables] class attribute:

use Mosamy\Translatable\Attributes\Translatables;

#[Translatables(attributes: ['title', 'description'])]
class Post extends Model
{
  use Translatable;
}

restrict controls fallback behavior for unknown attributes:

#[Translatables(attributes: ['title', 'description'], restrict: false)]
  • restrict: true (default): only listed attributes can fallback to translated values.
  • restrict: false: any missing attribute can fallback to translate($key).

Creating / Replacing Translations

Use createTranslations(array $translations) with this payload shape:

$post = Post::create(['status' => 'active']);

$post->createTranslations([
  'en' => [
    'title' => 'Post Title',
    'description' => 'Post Description',
  ],
  'fr' => [
    'title' => 'Titre de l\'article',
    'description' => 'Description du post',
  ],
]);

Behavior notes:

  • Existing translations for the model are deleted first, then new rows are inserted.
  • Falsy values are skipped (for example: null, '', 0, false).

Reading Translations

The trait automatically appends translations to model $with, so translations are eager-loaded by default.

$post = Post::find(1);

$post->translations;      // raw translation models
$post->translations_list; // grouped as locale => [attribute => body]

Example translations_list:

{
  "en": {
    "title": "Post Title",
    "description": "Post Description"
  },
  "fr": {
    "title": "Titre de l'article",
    "description": "Description du post"
  }
}

Attribute Fallback

getAttribute() fallback behavior:

  • If base model value is not null, the base value is returned.
  • If base model value is null, the trait may return a translated value depending on your #[Translatables(...)] config.
$post = Post::find(1);

echo $post->title;              // may fallback to translated value
echo $post->translate('title'); // current app locale
echo $post->translate('title', 'fr');

If no translation exists for locale/attribute, translate() returns null.

Query Scopes

whereTranslation($keyword, $attributes = [], $locale = [], $like = true)

Post::whereTranslation('keyword')->get();
Post::whereTranslation('keyword', ['description'])->get();
Post::whereTranslation('keyword', ['description'], ['ar', 'en'])->get();
Post::whereTranslation('keyword', ['description'], ['ar', 'en'], false)->get();

Notes:

  • If $attributes is empty and TranslatableAttributes constant exists, that constant is used.
  • If $locale is empty, current app locale is used.
  • $like = true uses LIKE %keyword%.
  • $like = false uses exact match (=).

hasTranslation($locale = null) and hasCurrentTranslation()

Post::hasTranslation()->get();
Post::hasTranslation('en')->get();
Post::hasCurrentTranslation()->get();

orderByTranslation($attribute, $sort = 'asc', $locale = null)

Post::orderByTranslation('title')->get();
Post::orderByTranslation('title', 'desc', 'fr')->get();

translateOnly($attributes)

Limit eager-loaded translations to specific attribute names:

Post::translateOnly('title')->get();
Post::translateOnly(['title', 'description'])->get();

Validation Rule

Use Mosamy\Translatable\Rules\Unique:

use Mosamy\Translatable\Rules\Unique as TranslationUnique;

public function rules(): array
{
  return [
    'translations.ar.title' => [
      'required',
      (new TranslationUnique(new Post()))->ignore($this->id),
    ],
    'translations.en.title' => [
      'required',
      (new TranslationUnique(new Post()))
        ->setLocale(['ar', 'en'])
        ->ignore($this->id),
    ],
  ];
}

Rule behavior:

  • If setLocale() is not called, locale is inferred from field path. Example: translations.ar.title -> locale ar.
  • If rule attribute is not passed in constructor, attribute is inferred from field path.
  • ignore($id) excludes a model ID during update checks.

Delete Behavior

Translations are automatically deleted when the parent model is deleted.

  • Normal delete: translations are deleted.
  • Soft delete: translations are also deleted (the cleanup runs on the model deleting event).

Manual deletion is also possible:

$post = Post::find(1);

$post->translations()->delete();
$post->translations()->where('locale', 'fr')->delete();

Breaking Changes / Upgrade Notes

  • Preferred configuration moved to PHP class attribute #[Translatables(...)].
  • Legacy TranslatableAttributes constant remains relevant for whereTranslation() default attributes.
  • Legacy $translatable property is still supported when class attribute is not used.
  • Automatic translated fallback now depends on restrict behavior and only happens when base attribute value is null.

Support

For support, contact dev.mohamed.samy@gmail.com.

Author

Created by Mohamed Samy