moonweft/translatable

A Laravel package for handling multilingual content using table-based translations

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/moonweft/translatable

1.0.0 2025-10-13 10:22 UTC

This package is not auto-updated.

Last update: 2025-10-14 08:41:39 UTC


README

A Laravel package for handling multilingual content using table-based translations. This package provides a clean and efficient way to manage translations for your Eloquent models without relying on JSON fields.

Features

  • Table-based translations: Store translations in separate database tables for better query performance and data integrity
  • Eloquent integration: Seamless integration with Laravel's Eloquent ORM
  • Query scopes: Built-in query scopes for filtering by translations
  • Custom validation rules: TranslatableUnique and TranslatableExists validation rules
  • Fallback support: Automatic fallback to default locale when translation is missing
  • Factory support: Eloquent Factory macros for creating translated models
  • 🌍 Extensive language support: Supports 70+ mainstream languages including RTL languages
  • 🔤 Unicode support: Full support for Unicode characters and special symbols
  • 🌐 Localized validation: Validation error messages in multiple languages
  • 📝 AbstractTranslation: Base class for translation models without timestamps
  • 🛠️ Make Command: make:moonweft-model command for generating complete model structures
  • Comprehensive testing: Full test coverage with 139 tests

Installation

composer require moonweft/translatable

Configuration

The package works out of the box with Laravel's default configuration. No additional configuration is required.

Basic Usage

1. Create Migration Files

First, create the main table migration:

// database/migrations/create_products_table.php
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('sku')->unique();
    $table->decimal('price', 10, 2);
    $table->integer('stock')->default(0);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

Then create the translations table:

// database/migrations/create_product_translations_table.php
Schema::create('product_translations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('product_id')->constrained()->onDelete('cascade');
    $table->string('locale', 10);
    $table->string('name')->nullable();
    $table->string('slug')->nullable();
    $table->text('description')->nullable();
    $table->text('content')->nullable();
    $table->text('specs')->nullable();
    $table->string('meta_title')->nullable();
    $table->text('meta_description')->nullable();
    $table->text('meta_keywords')->nullable();
    $table->text('short_description')->nullable();
    $table->text('features')->nullable();
    $table->text('benefits')->nullable();
    $table->timestamps();

    $table->unique(['product_id', 'locale']);
    $table->unique(['locale', 'slug']);
    $table->index(['locale', 'name']);
    $table->index(['locale', 'slug']);
});

2. Create Models

Create your main model:

// app/Models/Product.php
<?php

namespace App\Models;

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

class Product extends Model
{
    use Translatable;

    protected $fillable = [
        'sku',
        'price',
        'stock',
        'is_active',
    ];

    protected $casts = [
        'price' => 'decimal:2',
        'is_active' => 'boolean',
    ];

    public array $translatedAttributes = [
        'name',
        'slug',
        'description',
        'content',
        'specs',
        'meta_title',
        'meta_description',
        'meta_keywords',
        'short_description',
        'features',
        'benefits',
    ];

    public string $translationModel = ProductTranslation::class;
    public string $translationForeignKey = 'product_id';
    public string $localeKey = 'locale';
    public bool $useTranslationFallback = true;
}

Create the translation model:

// app/Models/ProductTranslation.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ProductTranslation extends Model
{
    protected $fillable = [
        'product_id',
        'locale',
        'name',
        'slug',
        'description',
        'content',
        'specs',
        'meta_title',
        'meta_description',
        'meta_keywords',
        'short_description',
        'features',
        'benefits',
    ];

    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

3. Working with Translations

Creating Products with Translations

Method 1: Step by step creation

// Create a product
$product = Product::create([
    'sku' => 'PROD-001',
    'price' => 100.0,
    'stock' => 50,
    'is_active' => true,
]);

// Add English translation
$product->translateOrNew('en')->name = 'English Product Name';
$product->translateOrNew('en')->slug = 'english-product-name';
$product->translateOrNew('en')->description = 'English description';
$product->save();

// Add Arabic translation
$product->translateOrNew('ar')->name = 'اسم المنتج بالعربية';
$product->translateOrNew('ar')->slug = 'اسم-المنتج-بالعربية';
$product->translateOrNew('ar')->description = 'وصف المنتج بالعربية';
$product->save();

Method 2: Bulk creation with translations

$data = [
    'sku' => 'PROD-001',
    'price' => 100.0,
    'stock' => 50,
    'brand' => 'TestBrand',
    'en' => [
        'name' => 'My first product',
        'slug' => 'my-first-product',
        'description' => 'This is my first product in English',
        'content' => 'Detailed content in English',
        'meta_title' => 'My First Product - English',
        'meta_description' => 'English meta description',
    ],
    'ar' => [
        'name' => 'منتجي الأول',
        'slug' => 'منتجي-الأول',
        'description' => 'هذا هو منتجي الأول بالعربية',
        'content' => 'محتوى تفصيلي بالعربية',
        'meta_title' => 'منتجي الأول - العربية',
        'meta_description' => 'وصف ميتا بالعربية',
    ],
    'fr' => [
        'name' => 'Mon premier produit',
        'slug' => 'mon-premier-produit',
        'description' => 'Ceci est mon premier produit en français',
        'content' => 'Contenu détaillé en français',
        'meta_title' => 'Mon Premier Produit - Français',
        'meta_description' => 'Description méta en français',
    ],
];

$product = Product::create($data);

// Access translations
echo $product->translate('fr')->name; // "Mon premier produit"
echo $product->translate('ar')->description; // "هذا هو منتجي الأول بالعربية"

Supported Languages

The package supports 70+ mainstream languages including:

European Languages:

  • English (en), Spanish (es), French (fr), German (de), Italian (it)
  • Portuguese (pt), Russian (ru), Dutch (nl), Swedish (sv), Danish (da)
  • Norwegian (no), Finnish (fi), Polish (pl), Czech (cs), Hungarian (hu)
  • Romanian (ro), Bulgarian (bg), Croatian (hr), Slovak (sk), Slovenian (sl)
  • Estonian (et), Latvian (lv), Lithuanian (lt), Ukrainian (uk)

Asian Languages:

  • Chinese Simplified (zh_CN), Chinese Traditional (zh_TW)
  • Japanese (ja), Korean (ko), Hindi (hi), Thai (th), Vietnamese (vi)
  • Indonesian (id), Bengali (bn), Tamil (ta), Telugu (te), Marathi (mr)
  • Gujarati (gu), Kannada (kn), Malayalam (ml), Punjabi (pa)

Middle Eastern & African Languages:

  • Arabic (ar), Hebrew (he), Persian (fa), Turkish (tr)
  • Urdu (ur), Swahili (sw), Afrikaans (af), Amharic (am)

RTL Language Support: The package fully supports Right-to-Left (RTL) languages including Arabic, Hebrew, Persian, and Urdu.

// RTL language example
$product = Product::create([
    'sku' => 'RTL-PRODUCT-001',
    'price' => 100,
    'ar' => [
        'name' => 'منتج باللغة العربية',
        'description' => 'وصف المنتج باللغة العربية مع دعم كامل للنصوص من اليمين إلى اليسار',
    ],
    'he' => [
        'name' => 'מוצר בעברית',
        'description' => 'תיאור המוצר בעברית עם תמיכה מלאה בטקסט מימין לשמאל',
    ],
]);

echo $product->translate('ar')->name; // "منتج باللغة العربية"
echo $product->translate('he')->name; // "מוצר בעברית"

Unicode Support: Full support for Unicode characters and special symbols:

$product = Product::create([
    'sku' => 'UNICODE-PRODUCT-001',
    'price' => 100,
    'zh_CN' => [
        'name' => '中文产品名称',
        'description' => '这是中文产品描述,包含特殊字符:!@#¥%……&*()',
    ],
    'ja' => [
        'name' => '日本語製品名',
        'description' => 'これは日本語の製品説明です。特殊文字:!?「」【】',
    ],
    'th' => [
        'name' => 'ชื่อผลิตภัณฑ์ภาษาไทย',
        'description' => 'นี่คือคำอธิบายผลิตภัณฑ์ภาษาไทย พร้อมอักขระพิเศษ:!?「」【】',
    ],
]);

Retrieving Translations

// Get translation for current locale
$product = Product::find(1);
$name = $product->name; // Returns name in current locale

// Get translation for specific locale
app()->setLocale('ar');
$arabicName = $product->name; // Returns Arabic name

// Get translation object
$translation = $product->translate('en');
if ($translation) {
    echo $translation->name;
}

// Get translation or create new one
$translation = $product->translateOrNew('fr');
$translation->name = 'French Product Name';
$product->save();

// Bulk update with translations
$updateData = [
    'price' => 200.0,
    'stock' => 100,
    'en' => [
        'name' => 'Updated Product Name',
        'description' => 'Updated description in English',
        'content' => 'Updated content in English',
    ],
    'ar' => [
        'name' => 'اسم المنتج المحدث',
        'description' => 'وصف محدث بالعربية',
        'content' => 'محتوى محدث بالعربية',
    ],
];

$product->update($updateData);

Querying with Translations

// Find products with specific translation
$products = Product::whereTranslation('name', 'English Product Name')->get();

// Find products translated in specific locale
$products = Product::translatedIn('en')->get();

// Find products not translated in specific locale
$products = Product::notTranslatedIn('ar')->get();

// Find products with translation like
$products = Product::whereTranslationLike('name', '%Product%')->get();

// Order by translation
$products = Product::orderByTranslation('name', 'asc')->get();

// Include translations in query
$products = Product::withTranslation()->get();

Validation Rules

The package provides custom validation rules for translatable fields:

TranslatableUnique

Validates that a translatable field is unique within a specific locale:

use Moonweft\Translatable\Validation\Rules\TranslatableUnique;
use Illuminate\Support\Facades\Validator;

$validator = Validator::make([
    'name' => 'Product Name',
], [
    'name' => new TranslatableUnique(Product::class, 'name'),
]);

// With specific locale
$validator = Validator::make([
    'name' => 'Product Name',
], [
    'name' => new TranslatableUnique(Product::class, 'name', null, null, 'en'),
]);

// Using Rule factory
use Moonweft\Translatable\Validation\RuleFactory;

$validator = Validator::make([
    'slug' => 'product-slug',
], [
    'slug' => RuleFactory::unique(Product::class, 'slug'),
]);

TranslatableExists

Validates that a translatable field exists within a specific locale:

use Moonweft\Translatable\Validation\Rules\TranslatableExists;

$validator = Validator::make([
    'category' => 'Electronics',
], [
    'category' => new TranslatableExists(Category::class, 'name'),
]);

Eloquent Factory Support

The package provides macros for Eloquent factories to easily create translated models:

// In your factory
use App\Models\Product;

Product::factory()
    ->withTranslations([
        'en' => [
            'name' => 'English Product',
            'description' => 'English description',
        ],
        'ar' => [
            'name' => 'منتج إنجليزي',
            'description' => 'وصف إنجليزي',
        ],
    ])
    ->create();

Advanced Usage

AbstractTranslation - Translation Models Without Timestamps

For translation models that don't need created_at and updated_at fields, use the AbstractTranslation base class:

<?php

namespace App\Models;

use Moonweft\Translatable\AbstractTranslation;

class ProductTranslation extends AbstractTranslation
{
    protected $fillable = [
        'product_id',
        'locale',
        'name',
        'slug',
        'description',
        'content',
        'meta_title',
        'meta_description',
    ];

    protected function getTranslationTableName(): string
    {
        return 'product_translations';
    }

    public function getForeignKeyName(): string
    {
        return 'product_id';
    }

    public function getTranslatableAttributes(): array
    {
        return [
            'name',
            'slug',
            'description',
            'content',
            'meta_title',
            'meta_description',
        ];
    }

    public function getMainModelClass(): string
    {
        return Product::class;
    }
}

Key Features of AbstractTranslation:

  • No timestamps: Automatically disables created_at and updated_at
  • Clean attributes: All attribute methods exclude timestamps
  • Type safety: Proper type hints and return types
  • Extensible: Easy to extend with custom methods
  • Performance: Slightly better performance without timestamp handling

Migration without timestamps:

Schema::create('product_translations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('product_id')->constrained()->onDelete('cascade');
    $table->string('locale', 10);
    $table->string('name')->nullable();
    $table->string('slug')->nullable();
    $table->text('description')->nullable();
    $table->longText('content')->nullable();
    $table->string('meta_title')->nullable();
    $table->text('meta_description')->nullable();
    
    $table->unique(['product_id', 'locale']);
    $table->index(['locale', 'name']);
    $table->index(['locale', 'slug']);
    // No timestamps() call
});

Custom Translation Model

You can customize the translation model by overriding the translationModel property:

class Product extends Model
{
    use Translatable;
    
    public string $translationModel = CustomProductTranslation::class;
    // ... other properties
}

Fallback Locale

Enable fallback to default locale when translation is missing:

class Product extends Model
{
    use Translatable;
    
    public bool $useTranslationFallback = true;
    // ... other properties
}

Bulk Operations

// Delete all translations
$product->deleteTranslations();

// Delete specific translation
$product->deleteTranslation('en');

// Get all translations as array
$translations = $product->getTranslationsArray();

Testing

The package includes comprehensive tests. Run the test suite:

php vendor/bin/phpunit

Performance Testing

The package includes extensive performance tests to ensure optimal performance:

# Run basic performance tests
php vendor/bin/phpunit tests/PerformanceTest.php --testdox

# Run detailed benchmark suite with metrics
VERBOSE_PERFORMANCE=1 php vendor/bin/phpunit tests/PerformanceBenchmark.php --testdox

# Run high concurrency tests
VERBOSE_PERFORMANCE=1 php vendor/bin/phpunit tests/HighConcurrencyTest.php --testdox

# Run big data performance tests
VERBOSE_PERFORMANCE=1 php vendor/bin/phpunit tests/BigDataTest.php --testdox

# Run stress tests
VERBOSE_PERFORMANCE=1 php vendor/bin/phpunit tests/StressTest.php --testdox

# Run HTTP concurrency tests
VERBOSE_PERFORMANCE=1 php vendor/bin/phpunit tests/HttpConcurrencyTest.php --testdox

# Run ReactPHP asynchronous concurrency tests
VERBOSE_PERFORMANCE=1 php vendor/bin/phpunit tests/ReactConcurrencyTest.php --testdox

# Run all tests with performance metrics
VERBOSE_PERFORMANCE=1 php vendor/bin/phpunit --testdox

Performance Highlights:

  • ✅ 100 products with translations created in ~32ms
  • ✅ 50 product updates in ~20ms
  • ✅ Sub-millisecond translation queries
  • ✅ Low memory footprint
  • ✅ Scales to 500+ products efficiently
  • High Concurrency: 100% success rate with 20 concurrent processes
  • Data Integrity: No race conditions or data corruption
  • Big Data Ready: 2,000+ records per second throughput
  • Enterprise Scalable: Handles 1,000+ products efficiently
  • Stress Tested: 100% success rate under extreme load conditions
  • High Throughput: Up to 22,831 operations per second
  • HTTP Concurrent: 100% success rate with concurrent HTTP requests
  • Real Concurrency: Up to 4,579 operations per second in HTTP scenarios
  • ReactPHP Async: 100% success rate with asynchronous concurrent operations
  • True Async Concurrency: Up to 4,509 operations per second in ReactPHP scenarios

See PERFORMANCE_REPORT.md for detailed performance metrics.

Make Command

The package includes a powerful make:moonweft-model command for generating complete model structures:

Basic Usage

# Create a basic model
php artisan make:moonweft-model Post --path=modules/posts

# Create a complete model with translation support
php artisan make:moonweft-model Post --path=modules/posts --translation --migration --controller --factory

Vendor Mode

Use the --vendor parameter to create models with vendor-based namespaces and minimal fields:

# Create a vendor-based model (namespace: Moonweft\Post\Models)
php artisan make:moonweft-model Post --path=modules/posts --vendor=moonweft --translation --migration --controller --factory

Vendor Mode Features:

  • Vendor-based namespace: Moonweft\Post\Models
  • Minimal main model: Only id and timestamps fields
  • Minimal translation model: Only id, locale, and xxx_id fields
  • Clean structure: No extra fields, user can add their own
  • Test files: Automatically generates test files in tests/ directory

Command Options

  • --path=: Specify the path where the model should be created
  • --vendor=: Use vendor-based namespace (e.g., moonweft)
  • --translation: Create translation model (extends AbstractTranslation)
  • --migration: Create migration files
  • --controller: Create controller
  • --factory: Create factory

Generated Files

The command creates a well-organized structure:

modules/posts/
├── src/
│   ├── Models/
│   │   ├── Post.php
│   │   └── PostTranslation.php (extends AbstractTranslation)
│   └── Http/
│       └── Controllers/
│           └── PostController.php
└── databases/
    ├── migrations/
    │   ├── 2025xxxx_create_posts_table.php
    │   └── 2025xxxx_create_posts_translations_table.php
    └── factories/
        └── PostFactory.php

Vendor Mode Example

php artisan make:moonweft-model Post --path=modules/posts --vendor=moonweft --translation --migration --controller --factory

Generated Model (Post.php):

<?php

namespace Moonweft\Post\Models;

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

class Post extends Model
{
    use Translatable;

    protected $table = 'posts';
    protected $fillable = []; // No fields, only translations

    public array $translatedAttributes = [
        'name',
        'slug',
        'description',
        'content',
        'meta_title',
        'meta_description',
    ];

    public string $translationModel = PostTranslation::class;
    public string $translationForeignKey = 'posts_id';
    public string $localeKey = 'locale';
    public bool $useTranslationFallback = true;
}

Generated Migration:

Schema::create('posts', function (Blueprint $table) {
    $table->id(); // Only id and timestamps
    $table->timestamps();
});

Generated Translation Migration:

Schema::create('posts_translations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->onDelete('cascade');
    $table->string('locale', 10);
    
    $table->unique(['post_id', 'locale']);
});

Generated Test Files:

  • modules/posts/tests/PostTest.php - Model tests
  • modules/posts/tests/PostControllerTest.php - Controller tests (if controller created)

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This package is open-sourced software licensed under the MIT license.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Credits

Support

If you discover any issues or have questions, please open an issue on GitHub.