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
Requires
- php: ^8.2
- illuminate/database: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
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
andupdated_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
andtimestamps
fields - Minimal translation model: Only
id
,locale
, andxxx_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 testsmodules/posts/tests/PostControllerTest.php
- Controller tests (if controller created)
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - 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.