📋 Overview
The Laravel Multilingual Framework is a complete out-of-the-box solution for building multilingual Laravel applications. This isn't just a translation tool - it's a comprehensive system that extends Laravel's default localization with automatic routing, AI-powered translations, middleware, SEO optimization, and more.
Core Components
🛣️ Smart Routing System
Automatic language-prefixed URLs with the langRoute() helper. One function creates routes for all languages.
🔐 Language Middleware
Intelligent language detection and switching. Handles SEO-friendly English URLs and automatic locale setting.
🤖 AI-Powered Translation
Uses OpenAI GPT models to translate strings accurately and contextually with automatic string collection.
🎨 Global View Helpers
Ready-to-use $langUrl(), $isRtl, $isRoute() helpers in all views.
📊 Translation Dashboard
Real-time progress monitoring with visual feedback and status updates using Livewire.
🌍 SEO Optimized
Auto-generated hreflang tags, canonical URLs, and x-default handling for search engines.
Key Features
- Automatic Routing: langRoute() helper creates all language routes automatically
- Language Middleware: Detects and sets locale based on URL prefix
- View Helpers: $langUrl(), $isRtl, $langCode, $isRoute() available globally
- AI Translation: OpenAI-powered translations with batch processing
- String Collection: Automatic extraction using @__t() directive
- RTL Support: Built-in right-to-left language support
- SEO Tags: Automatic hreflang and canonical URL generation
- Queue-Based: Background processing with rate limiting
⚙️ Installation
Step 1: Database Migration
Create the progress tracking table:
php artisan make:migration create_translation_progress_table
Add the migration code:
Schema::create('translation_progress', function (Blueprint $table) {
$table->id();
$table->string('type'); // 'string_extraction' or 'translation'
$table->string('locale')->nullable();
$table->integer('total')->default(0);
$table->integer('completed')->default(0);
$table->integer('failed')->default(0);
$table->timestamp('started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Run the migration:
php artisan migrate
Step 2: Add Blade Directive
In app/Providers/AppServiceProvider.php, add:
use Illuminate\Support\Facades\Blade;
public function boot(): void
{
// Translation collection mode - Blade directive
Blade::directive('__t', function ($expression) {
if (config('translation.translation_collection_mode', false)) {
return "' . __({$expression}); ?>";
}
return "";
});
}
Step 3: Environment Configuration
Add to your .env file:
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_MODEL=gpt-4o-mini
TRANSLATION_URL_DELAY=1
TRANSLATION_COLLECTION_MODE=false
QUEUE_CONNECTION=database
Step 4: Create Language Files
Create the initial JSON files in the lang/ directory:
// lang/en.json
{}
// lang/ar.json
{}
// lang/es.json (optional)
{}
🛣️ Smart Routing System
The heart of the multilingual framework is the langRoute() helper that automatically creates language-prefixed routes.
The langRoute() Helper Function
Add this function to your routes/web.php file:
/**
* Language Route Helper
* Creates both non-prefixed (English) and language-prefixed routes
* Example: /about and /ar/about
*/
function langRoute($method, $path, $action, $name = null, $where = []) {
$allowedLangs = array_keys(config('translation.languages'));
// 1. Main route (no language prefix - English)
$mainRoute = Route::$method($path, $action)->middleware('language');
if ($name) $mainRoute->name($name);
if (!empty($where)) $mainRoute->where($where);
// 2. Prefixed routes for each language
foreach ($allowedLangs as $lang) {
$langRoute = Route::$method('/' . $lang . $path, $action)->middleware('language');
if ($name) $langRoute->name($lang . '.' . $name);
if (!empty($where)) $langRoute->where($where);
}
return $mainRoute;
}
How It Works
Instead of defining routes multiple times for each language:
// ❌ Old way (repetitive)
Route::get('/about', About::class)->name('about');
Route::get('/ar/about', About::class)->name('ar.about');
Route::get('/es/about', About::class)->name('es.about');
Use langRoute() once:
// ✅ New way (automatic)
langRoute('get', '/about', About::class, 'about');
Route Examples
// Basic routes
langRoute('get', '/', HomePage::class, 'home');
langRoute('get', '/about', About::class, 'about');
langRoute('get', '/contact', Contact::class, 'contact');
// Routes with parameters
langRoute('get', '/products/{slug}', ProductShow::class, 'products.show');
langRoute('get', '/blog/{category}/{slug}', BlogPost::class, 'blog.post');
// Routes with middleware
langRoute('get', '/dashboard', Dashboard::class, 'dashboard')
->middleware(['auth', 'verified']);
// Routes with constraints
langRoute('get', '/user/{id}', UserProfile::class, 'user.profile', ['id' => '[0-9]+']);
// POST routes
langRoute('post', '/contact', ContactSubmit::class, 'contact.submit');
What Gets Created Automatically
| Your Code | Generated Routes |
|---|---|
| langRoute('get', '/about', ...) |
/about → about /ar/about → ar.about /es/about → es.about |
| langRoute('get', '/products/{slug}', ...) |
/products/{slug} → products.show /ar/products/{slug} → ar.products.show /es/products/{slug} → es.products.show |
Generating Language-Specific URLs
Use the $langUrl() helper in your Blade views:
<!-- Simple link -->
<a href="{{ $langUrl('about') }}">@__t('About Us')</a>
<!-- With parameters -->
<a href="{{ $langUrl('products.show', ['slug' => $product->slug]) }}">
{{ $product->name }}
</a>
<!-- Result when viewing in Arabic: -->
<!-- /ar/about -->
<!-- /ar/products/example-product -->
🔐 Language Middleware
The LanguageMiddleware handles automatic language detection and switching based on URL prefixes.
How the Middleware Works
- Detects Language Prefix: Checks if URL starts with a 2-letter language code
- Validates Language: Ensures the prefix matches configured languages
- Handles English URLs: Redirects /en/... to /... (SEO best practice)
- Sets Locale: Updates Laravel's active locale with app()->setLocale()
- Stores in Session: Persists language choice across requests
Middleware Code
Located at app/Http/Middleware/LanguageMiddleware.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
class LanguageMiddleware
{
public function handle(Request $request, Closure $next)
{
$allowedLangs = array_keys(config('translation.languages'));
$firstSegment = $request->segment(1);
// Only consider it a language prefix if:
// - It exists and is EXACTLY 2 lowercase letters
// - It matches one of our allowed languages
if (
$firstSegment &&
strlen($firstSegment) === 2 &&
ctype_lower($firstSegment) &&
in_array($firstSegment, $allowedLangs, true)
) {
$lang = $firstSegment;
// Special case: redirect /en/... to /... (SEO)
if ($lang === 'en') {
$pathWithoutLang = '/' . implode('/', array_slice($request->segments(), 1));
if ($pathWithoutLang === '/') $pathWithoutLang = '';
return redirect($pathWithoutLang ?: '/', 301);
}
// Valid non-English language prefix
Session::put('language', $lang);
app()->setLocale($lang);
return $next($request);
}
// Default: no valid language prefix → English
Session::put('language', 'en');
app()->setLocale('en');
return $next($request);
}
}
Registering the Middleware
Add to app/Http/Kernel.php:
protected $middlewareAliases = [
// ... other middleware
'language' => \App\Http\Middleware\LanguageMiddleware::class,
];
🎨 Global View Helpers
The system provides powerful helpers available in all your Blade views automatically.
Available Helpers
| Helper | Description | Example |
|---|---|---|
| $langCode | Current language code | 'en', 'ar', 'es' |
| $isRtl | RTL language detection | true or false |
| $langUrl($route, $params) | Generate language-specific URL | /ar/about |
| $isRoute($routeName) | Check if current route matches | true or false |
Real-World Header Example
Here's how to build a complete multilingual navigation header:
@php
$isRtl = in_array(app()->getLocale(), config('translation.rtl_languages', []));
@endphp
<header class="{{ $isRtl ? 'flex-row-reverse' : '' }}">
<nav>
<a href="{{ $langUrl('home') }}"
class="{{ $isRoute('home') ? 'active' : '' }}">
@__t('Home')
</a>
<a href="{{ $langUrl('about') }}"
class="{{ $isRoute('about') ? 'active' : '' }}">
@__t('About')
</a>
<a href="{{ $langUrl('contact') }}"
class="{{ $isRoute('contact') ? 'active' : '' }}">
@__t('Contact')
</a>
</nav>
</header>
Language Switcher Example
Create a dropdown menu for language selection:
<div class="language-selector">
@foreach(config('translation.languages') as $locale => $name)
@php
$currentPath = request()->path();
$currentLang = app()->getLocale();
// Remove current language prefix
if ($currentLang !== 'en') {
$currentPath = preg_replace('#^' . $currentLang . '(/|$)#', '', $currentPath);
}
// Build URL for target language
if ($locale === 'en') {
$switchUrl = '/' . $currentPath;
} else {
$switchUrl = '/' . $locale . '/' . $currentPath;
}
$switchUrl = preg_replace('#/+#', '/', $switchUrl);
$isActive = (app()->getLocale() === $locale);
@endphp
<a href="{{ $switchUrl }}" class="{{ $isActive ? 'active' : '' }}">
{{ config('translation.language_names.' . $langCode . '.' . $locale) }}
</a>
@endforeach
</div>
RTL Support Example
Automatically adjust layout for right-to-left languages:
<!-- Conditional classes for RTL -->
<div class="container {{ $isRtl ? 'text-right' : 'text-left' }}">
@if($isRtl)
<div dir="rtl">
<p>@__t('This content is in Arabic')</p>
</div>
@else
<div dir="ltr">
<p>@__t('This content is in English')</p>
</div>
@endif
</div>
<!-- TailwindCSS RTL utilities -->
<div class="{{ $isRtl ? 'space-x-reverse' : '' }} space-x-4">
<button class="{{ $isRtl ? 'mr-auto' : 'ml-auto' }}">
@__t('Submit')
</button>
</div>
🔧 Configuration
Config File: config/translation.php
The system uses a centralized configuration file:
return [
// Enable detailed logging for debugging
'log_process' => env('TRANSLATION_LOG_PROCESS', false),
// Translation collection mode toggle
'translation_collection_mode' => env('TRANSLATION_COLLECTION_MODE', false),
// Source language for your application
'source_locale' => 'en',
// All available languages with their full names
'languages' => [
'en' => 'English',
'ar' => 'Arabic',
'es' => 'Spanish',
'fr' => 'French',
'de' => 'German',
// Add more languages as needed
],
// How each language name appears in each language
'language_names' => [
'en' => [
'en' => 'English',
'ar' => 'Arabic',
'es' => 'Spanish',
],
'ar' => [
'en' => 'الإنجليزية',
'ar' => 'العربية',
'es' => 'الإسبانية',
],
],
// Which languages to translate to (excluding source)
'target_locales' => ['ar', 'es', 'fr', 'de'],
// RTL (Right-to-Left) languages
'rtl_languages' => ['ar', 'he', 'ur'],
// Language file paths
'language_files' => [
'en' => lang_path('en.json'),
'ar' => lang_path('ar.json'),
'es' => lang_path('es.json'),
// Add more as needed
],
// URL collection settings
'urls' => [
'delay_between_requests' => env('TRANSLATION_URL_DELAY', 1),
'batch_size' => 50,
'timeout' => 30,
],
// String extraction settings
'extraction' => [
'scan_internal' => true,
'clear_cache' => true,
],
// AI translation settings
'translation' => [
'ai_provider' => 'openai',
'model' => env('OPENAI_MODEL', 'gpt-4o-mini'),
'api_key' => env('OPENAI_API_KEY'),
'batch_size' => 20,
'rate_limit_per_minute' => 300,
'max_retries' => 3,
'system_prompt' => 'You are a professional translator. Translate the following text to {language}. Return ONLY the translated text with no explanations, greetings, or additional commentary. Preserve any HTML tags, placeholders like :name, and formatting.',
],
];
Configuration Options Explained
| Option | Description | Default |
|---|---|---|
| source_locale | The source language of your application | en |
| target_locales | Array of languages to translate to | ['ar', 'es'] |
| delay_between_requests | Seconds to wait between URL scans | 1 |
| batch_size | Number of strings per translation batch | 20 |
| rate_limit_per_minute | Maximum API calls to OpenAI per minute | 300 |
📖 Usage Guide
Step 1: Generate URLs
Navigate to the translation dashboard at /translation-dashboard
Option A: Manual URLs
Enter URLs one per line in the "Manual URLs" textarea:
https://yoursite.com/home
https://yoursite.com/about
https://yoursite.com/contact
Option B: API Endpoints
Enter API endpoints that return arrays of URLs:
https://yoursite.com/api/sitemap/articles
https://yoursite.com/api/sitemap/products
articles|https://yoursite.com/api/sitemap/articles
Click 🔗 Generate URLs button. You should see confirmation:
Step 2: Collect Translation Strings
Start Queue Worker
Open a terminal and run:
php artisan queue:work --tries=3 --verbose
Start Collection
Click 📝 Collect Strings button in the dashboard.
The system will:
- Set started_at timestamp in database
- Visit each URL in your list
- Extract all strings wrapped with @__t()
- Save unique strings to lang/en.json
- Update updated_at on each job completion
- Set completed_at when all URLs processed
Monitor progress with the green progress bar showing completion percentage.
Step 3: Translate All Keys
Once string collection is complete, click 🌐 Translate All Keys
The system will:
- Read all keys from lang/en.json
- Find untranslated keys in target languages
- Send batches to OpenAI for translation
- Track progress with timestamps (started_at, updated_at, completed_at)
- Save translations to respective JSON files
You'll see individual progress bars for each language (Arabic, Spanish, etc.)
Step 4: Verify Translations
Check your language files:
// lang/en.json
{
"Welcome to our site": "Welcome to our site",
"I love programming.": "I love programming."
}
// lang/ar.json
{
"Welcome to our site": "مرحبا بكم في موقعنا",
"I love programming.": "أنا أحب البرمجة."
}
🔍 How It Works
The Translation Flow
1. URL Collection
The URLCollector service gathers all URLs to scan:
- Accepts manual URL input
- Fetches URLs from API endpoints
- Merges and deduplicates all URLs
- Saves to config/urls.json
2. String Extraction
The StringExtractor visits each URL:
- Enable Collection Mode: Temporarily sets Config::set('app.translation_collection_mode', true) for this request only
- Clear Cache: Optionally runs view:clear to ensure Blade directive works
- Internal Request: Makes Laravel internal request (no HTTP overhead)
- Extract Keys: Uses regex to find all HTML comment markers <!--T_START:...:T_END-->
- Disable Collection Mode: Immediately disables collection mode after extraction
- Save to JSON: Adds new keys to lang/en.json
3. Blade Directive Magic
The @__t() directive works differently based on mode:
Collection Mode OFF (Production):
<h2>@__t('Hello World')</h2>
// Renders as: <h2>Hello World</h2>
Collection Mode ON (Scanning):
<h2>@__t('Hello World')</h2>
// Renders as: <h2><!--T_START:Hello World:T_END-->Hello World</h2>
4. AI Translation Process
The AITranslator service:
- Loads untranslated keys from lang/en.json
- Splits into batches (default: 20 strings per batch)
- Sends each batch to OpenAI with custom prompt
- Applies rate limiting (default: 300 requests/minute)
- Saves translations to target language files
- Retries failed translations up to 3 times
5. Queue Processing & Progress Tracking
Jobs are processed via Laravel queues with detailed progress tracking:
- ScanUrlForStringsJob: Extracts strings from URLs
- TranslateStringBatchJob: Translates batches of strings
- started_at: Set when batch processing begins
- updated_at: Updated on every single job completion
- completed_at: Set when completed == total (all jobs done)
📚 API Reference
Services
URLCollector
| Method | Parameters | Description |
|---|---|---|
| collectFromAPIs() | array $apiEndpoints | Fetches URLs from API endpoints |
| addManualUrls() | array $manualUrls | Adds manually entered URLs |
| saveToConfig() | - | Saves URLs to config/urls.json |
| loadFromConfig() | - | Loads URLs from config file |
StringExtractor
| Method | Parameters | Description |
|---|---|---|
| extractFromUrl() | string $url | Extracts translatable strings from URL using HTML comment markers |
| saveToLanguageFile() | array $keys, string $locale | Saves keys to language JSON file |
| getAllKeys() | string $locale | Returns all keys from language file |
AITranslator
| Method | Parameters | Description |
|---|---|---|
| translate() | string $text, string $locale | Translates single string to target language |
| translateBatch() | array $texts, string $locale | Translates multiple strings at once |
| isConfigured() | - | Checks if OpenAI API key is set |
| getTargetLocales() | - | Returns array of target languages |
Jobs
ScanUrlForStringsJob
// Dispatch manually
use App\Jobs\ScanUrlForStringsJob;
ScanUrlForStringsJob::dispatch('https://yoursite.com/page', $delaySeconds = 1)
->onQueue('strings');
This job updates progress tracking:
- Increments completed count
- Updates updated_at timestamp
- Sets completed_at when finished
TranslateStringBatchJob
// Dispatch manually
use App\Jobs\TranslateStringBatchJob;
$strings = [
'Hello' => 'Hello',
'Welcome' => 'Welcome'
];
TranslateStringBatchJob::dispatch($strings, 'ar')
->onQueue('translations');
Blade Directive
Usage in Views
{{-- Simple string --}}
<h1>@__t('Welcome to our site')</h1>
{{-- With variables --}}
<p>@__t('Hello ' . $userName)</p>
{{-- Dynamic content --}}
<span>@__t($pageTitle)</span>
🔧 Troubleshooting
Common Issues
1. Progress Bar Stuck at 0%
Solution: Queue worker is not running. Start it:
php artisan queue:work --queue=strings,translations --tries=3
2. No Strings Extracted
Possible causes:
- Not using @__t() directive in Blade files
- Blade cache not cleared
- Collection mode not enabled during scan
Solution:
// 1. Verify you're using @__t() in views
<h1>@__t('My Title')</h1>
// 2. Clear Blade cache
php artisan view:clear
// 3. Check logs
tail -f storage/logs/laravel.log
// 4. Look for HTML comment markers
<!--T_START:My Title:T_END-->
3. Translation Fails
Solution: Check your API key and quota:
// Verify .env settings
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini
// Check failed jobs
php artisan queue:failed
4. Rate Limit Exceeded
Solution: Adjust rate limiting in config:
// config/translation.php
'translation' => [
'rate_limit_per_minute' => 100, // Reduce this
'batch_size' => 10, // Smaller batches
]
5. Memory Issues
Solution:
// Increase PHP memory limit
php artisan queue:work --memory=512
// Or restart worker periodically
php artisan queue:work --max-time=3600
6. Progress Not Updating
Solution: Check database timestamps:
// In tinker
DB::table('translation_progress')->get();
// Should show started_at, updated_at, completed_at
// updated_at should change on every job completion
Debugging Tips
Enable Verbose Logging
// Check extraction logs
grep "Extracted keys" storage/logs/laravel.log
// Check translation logs
grep "Translated" storage/logs/laravel.log
// Check progress updates
grep "updated_at" storage/logs/laravel.log
// Check errors
grep "ERROR" storage/logs/laravel.log
Test Individual Components
// Test URL extraction
php artisan tinker
$extractor = new App\Services\Translation\StringExtractor();
$keys = $extractor->extractFromUrl('http://127.0.0.1:8000/home');
dd($keys);
// Test translation
$translator = new App\Services\Translation\AITranslator();
$result = $translator->translate('Hello World', 'ar');
dd($result);
Check Database Progress
php artisan tinker
$progress = DB::table('translation_progress')->get();
foreach ($progress as $p) {
echo "{$p->type} ({$p->locale}): {$p->completed}/{$p->total}\n";
echo "Started: {$p->started_at}\n";
echo "Updated: {$p->updated_at}\n";
echo "Completed: {$p->completed_at}\n\n";
}
🚀 Advanced Usage
Custom API Sitemap Endpoints
Create API endpoints that return arrays of URLs for automatic discovery:
// routes/api.php
Route::get('/sitemap/articles', function () {
return Article::all()->map(fn($a) => url("/article/{$a->slug}"));
});
Route::get('/sitemap/products', function () {
return Product::all()->map(fn($p) => url("/product/{$p->id}"));
});
Programmatic Usage
Collect Strings Programmatically
use App\Services\Translation\URLCollector;
use App\Services\Translation\StringExtractor;
use App\Jobs\ScanUrlForStringsJob;
$collector = new URLCollector();
$collector->addManualUrls([
'https://yoursite.com/page1',
'https://yoursite.com/page2',
]);
$collector->saveToConfig();
// Dispatch jobs
$urls = $collector->loadFromConfig();
foreach ($urls as $url) {
ScanUrlForStringsJob::dispatch($url, 1)->onQueue('strings');
}
Translate Specific Keys
use App\Services\Translation\AITranslator;
$translator = new AITranslator();
// Single translation
$arabic = $translator->translate('Welcome to our site', 'ar');
// Batch translation
$strings = [
'Hello' => 'Hello',
'Goodbye' => 'Goodbye',
];
$translations = $translator->translateBatch($strings, 'es');
Monitoring Progress Programmatically
use Illuminate\Support\Facades\DB;
// Get current extraction progress
$extraction = DB::table('translation_progress')
->where('type', 'string_extraction')
->whereNull('locale')
->first();
echo "Extraction: {$extraction->completed}/{$extraction->total}\n";
echo "Started: {$extraction->started_at}\n";
echo "Last updated: {$extraction->updated_at}\n";
if ($extraction->completed_at) {
echo "Completed: {$extraction->completed_at}\n";
}
// Get translation progress for Arabic
$translation = DB::table('translation_progress')
->where('type', 'translation')
->where('locale', 'ar')
->first();
echo "Arabic translation: {$translation->completed}/{$translation->total}\n";
Custom Translation Providers
You can extend the system to use other translation APIs:
// Create custom translator
class GoogleTranslator extends AITranslator
{
protected function callAPI(string $text, string $locale): ?string
{
// Implement Google Translate API
// Return translated text
}
}
Scheduling Automatic Updates
Set up automatic translation updates using Laravel scheduler:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// Collect new strings daily
$schedule->command('translation:collect')->daily();
// Translate weekly
$schedule->command('translation:translate')->weekly();
}
Performance Optimization
Use Redis for Queues
// .env
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis
// Run multiple workers for parallel processing
php artisan queue:work redis --tries=3 &
php artisan queue:work redis --tries=3 &
php artisan queue:work redis --tries=3 &
Optimize Batch Sizes
// config/translation.php
'translation' => [
'batch_size' => 50, // Increase for faster processing
'rate_limit_per_minute' => 1000, // If your API allows
]
// Run multiple queue workers for parallel processing
php artisan queue:work --tries=3 &
php artisan queue:work --tries=3 &
php artisan queue:work --tries=3 &
Webhook Notifications
Get notified when translation jobs complete:
// In your job
public function handle()
{
// ... translation logic
// Send webhook on completion
Http::post('https://your-webhook-url.com', [
'status' => 'completed',
'locale' => $this->locale,
'translated_count' => count($this->strings),
'completed_at' => now()
]);
}
Multi-Tenant Support
Handle translations for multiple tenants/projects:
// Separate language files per tenant
'language_files' => [
'tenant1_en' => storage_path('app/translations/tenant1/en.json'),
'tenant1_ar' => storage_path('app/translations/tenant1/ar.json'),
'tenant2_en' => storage_path('app/translations/tenant2/en.json'),
]
HTML Comment Markers in Production
While HTML comments have zero SEO or CSS impact, you can still disable them in production:
// .env.production
TRANSLATION_COLLECTION_MODE=false
// .env.staging
TRANSLATION_COLLECTION_MODE=true
This way you only have comment markers when actively collecting strings, and clean HTML in production.