Laravel AI Translate v3.0
Ctrl K
warmardev.com
Open Source

Laravel AI
Translation System

A complete multilingual framework built on top of Laravel's default translation system. Uses standard __() everywhere — no custom directives, no proprietary syntax. AI-powered translations, automatic routing, SEO optimization, and RTL support out of the box.

PHP 8.2+ Laravel 11+ / 12+ Livewire 3/4 OpenAI GPT-4 TailwindCSS Standard __() Syntax

AI-Powered

OpenAI GPT-4 translations with high-quality results and rate limiting.

Database-Backed

Eloquent models, indexed tables, full CRUD for URLs with API endpoint auto-fetching.

Smart Routing

Auto language-prefixed URLs, SEO hreflang tags, canonical URLs, and middleware.

Inline Editor

Edit translations directly in the dashboard with per-string AI translation.

Zero Custom Syntax

Uses standard Laravel __() — no proprietary directives, zero learning curve.

Missing Keys Detection

Passive collection via handleMissingKeysUsing — catches untranslated strings from live traffic automatically.

Requirements

  • PHP 8.2+
  • Laravel 11+ / 12+
  • OpenAI API Key
  • Queue worker (Redis recommended, Database queue supported)
  • Livewire 3.x / 4.x

Installation

1. Publish Laravel's Default Localization

php artisan lang:publish

2. Install the package via Composer

composer require warmar/laravel-ai-translate

3. Install the package assets

php artisan ai-translate:install

4. Register the Service Provider

Add to bootstrap/providers.php:

<?php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\Translate\TranslationServiceProvider::class,
];
Note: The TranslationServiceProvider hooks into Laravel's handleMissingKeysUsing for automatic string collection and missing key detection. You do not need to modify your AppServiceProvider.

5. Register Language Middleware

Add to bootstrap/app.php:

use App\Http\Middleware\Translate\LanguageMiddleware;

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'language' => LanguageMiddleware::class,
    ]);
})

6. Configure Environment

OPENAI_API_KEY=your-api-key-here
OPENAI_MODEL=gpt-4o-mini
Note: String collection is handled automatically at runtime via Laravel's handleMissingKeysUsing hook — no manual toggling needed. All other settings are in config/translation.php.

7. Register Global Helpers

Add to composer.json autoload:

"files": ["app/Helpers/Translate/TranslationHelper.php"]
composer dump-autoload

Global Helper Functions

FunctionReturnsDescription
langCode()stringCurrent locale code
isRtl()boolWhether current locale is RTL
langUrl($route, $params)stringLanguage-prefixed URL
isRoute($name)boolWhether current route matches

8. Queue Workers

# Development
composer run dev
# Production
php artisan queue:work

9. Run Migrations

php artisan migrate

10. Clear Cache

php artisan view:clear
php artisan config:clear
php artisan config:cache

Database Schema

The system uses three database tables managed via Eloquent models.

translation_urls

ColumnTypeDescription
idBIGINT UNSIGNEDAuto-increment primary key
urlTEXTThe URL to scan
activeTINYINT(1)Active for extraction (default: 1)
is_apiTINYINT0 = regular URL, 1 = API endpoint
created_atTIMESTAMPCreated
updated_atTIMESTAMPUpdated
Important: is_api = 0 URLs are scanned for strings. is_api = 1 endpoints are fetched for URL discovery — never scanned directly.

translation_progress

ColumnTypeDescription
typeENUM'string_extraction' or 'translation'
localeVARCHAR(10)Target locale (NULL for extraction)
totalINT UNSIGNEDTotal items
completedINT UNSIGNEDCompleted
failedINT UNSIGNEDFailed

missing_translations

Tracks missing translation keys detected automatically from live traffic via Laravel's handleMissingKeysUsing hook.

ColumnTypeDescription
idBIGINT UNSIGNEDAuto-increment primary key
keyVARCHAR(500)The untranslated string key
localeVARCHAR(10)Locale where translation was missing
occurrencesBIGINT UNSIGNEDTimes requested (auto-increments)
first_seenDATETIMEWhen first detected
last_seenDATETIMEMost recently requested (auto-updates)

Raw SQL

-- translation_urls
CREATE TABLE `translation_urls` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `url` TEXT NOT NULL,
    `active` TINYINT(1) NOT NULL DEFAULT 1,
    `is_api` TINYINT NOT NULL DEFAULT 0,
    `created_at` TIMESTAMP NULL,
    `updated_at` TIMESTAMP NULL,
    PRIMARY KEY (`id`),
    INDEX (`active`),
    INDEX (`is_api`),
    INDEX (`active`, `is_api`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- translation_progress
CREATE TABLE `translation_progress` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `type` ENUM('string_extraction', 'translation') NOT NULL,
    `locale` VARCHAR(10) NULL,
    `total` INT UNSIGNED NOT NULL DEFAULT 0,
    `completed` INT UNSIGNED NOT NULL DEFAULT 0,
    `failed` INT UNSIGNED NOT NULL DEFAULT 0,
    `started_at` TIMESTAMP NULL,
    `updated_at` TIMESTAMP NULL,
    `completed_at` TIMESTAMP NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY (`type`, `locale`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- missing_translations
CREATE TABLE `missing_translations` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `key` VARCHAR(500) NOT NULL,
    `locale` VARCHAR(10) NOT NULL DEFAULT 'en',
    `occurrences` BIGINT UNSIGNED NOT NULL DEFAULT 1,
    `first_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `last_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
        ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY (`key`, `locale`),
    INDEX (`locale`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

File Structure

your-laravel-app/
├── app/
│   ├── Helpers/Translate/TranslationHelper.php
│   ├── Http/Middleware/Translate/LanguageMiddleware.php
│   ├── Jobs/Translate/
│   │   ├── ScanUrlForStringsJob.php
│   │   └── TranslateStringBatchJob.php
│   ├── Livewire/Translate/TranslateMenu.php
│   ├── Models/Translate/
│   │   ├── TranslationUrl.php
│   │   ├── TranslationProgress.php
│   │   └── MissingTranslation.php
│   ├── Providers/Translate/TranslationServiceProvider.php
│   └── Services/Translate/
│       ├── AITranslator.php
│       ├── StringExtractor.php
│       └── URLCollector.php
├── config/translation.php
├── database/migrations/
│   ├── create_translation_urls_table.php
│   ├── create_translation_progress_table.php
│   └── create_missing_translations_table.php
├── lang/ (en.json, ar.json, ...)
└── resources/views/livewire/translate/
    └── translate-menu.blade.php

Configuration

config/translation.php controls all settings:

return [
    'log_process' => false,
    'source_locale' => 'en',
    'languages' => ['en' => 'English', 'ar' => 'Arabic'],
    'target_locales' => ['ar'],
    'rtl_languages' => ['ar', 'he'],
    'urls' => [
        'delay_between_requests' => 1,
        'batch_size' => 50,
        'timeout' => 20,
        'api_scan_internal' => true,  // Avoids artisan serve deadlock
    ],
    'extraction' => [
        'scan_internal' => true,
        'clear_cache' => false,
    ],
    'translation' => [
        'model' => env('OPENAI_MODEL', 'gpt-4o-mini'),
        'api_key' => env('OPENAI_API_KEY'),
        'batch_size' => 20,
        'rate_limit_per_minute' => 300,
    ],
];
api_scan_internal: When enabled, local API endpoints (127.0.0.1, localhost) are fetched via internal Laravel requests instead of HTTP — avoids single-threaded php artisan serve deadlock.

Adding New Languages

'languages' => [
    'en' => 'English',
    'ar' => 'Arabic',
    'es' => 'Spanish',  // New
],
'target_locales' => ['ar', 'es'],
touch lang/es.json

Routing System

langRoute() auto-creates routes for all configured languages:

// Instead of repeating:
Route::get('/about', About::class);
Route::get('/ar/about', About::class);

// Just write:
langRoute('get', '/about', About::class, 'about');

Examples

langRoute('get', '/', Home::class, 'home');
langRoute('get', '/products/{slug}', ProductShow::class, 'products.show');

// With middleware (6th parameter!)
langRoute('get', '/dashboard', Dashboard::class, 'dashboard', [], ['auth']);
⚠️ Middleware Security: Never chain ->middleware() on langRoute(). Pass middleware as the 6th parameter so it applies to ALL language routes.

Language Middleware

LanguageMiddleware detects language from URL prefix, sets locale, stores in session, redirects /en/... to /... (301).

  1. Detects 2-letter language prefix
  2. Validates against configured languages
  3. Redirects /en/.../...
  4. Sets app()->setLocale()
  5. Stores in session

View Helpers

{{-- Current language --}}
{{ langCode() }}

{{-- RTL detection --}}
@if(isRtl())
    <div dir="rtl">...</div>
@endif

{{-- Language-aware URLs --}}
<a href="{{ langUrl('about') }}">About</a>

{{-- Standard Laravel __() for translations --}}
<h1>{{ __('Welcome') }}</h1>
<p>{{ __('Hello, :name!', ['name' => $user->name]) }}</p>

{{-- Check current route --}}
@if(isRoute('about'))
    <li class="active">About</li>
@endif
Standard Laravel syntax: Uses {{ __('...') }} everywhere — no custom directives. The handleMissingKeysUsing hook automatically detects and collects any untranslated strings at runtime.

Usage

Step 1: Use Standard Laravel __() Syntax

{{-- Standard Laravel __() — collected automatically --}}
<h1>{{ __('Welcome to Our Website') }}</h1>
<p>{{ __('We provide the best service') }}</p>
<button>{{ __('Contact Us') }}</button>
No custom directives needed. The system hooks into Laravel's handleMissingKeysUsing — every __() call that can't find a key is automatically detected and collected. Your templates use 100% standard Laravel syntax.

Step 2: Open the Dashboard

http://your-app.com/translation-dashboard

Step 3: Add URLs

Paste regular URLs and click "Add URLs". For dynamic content, add API endpoints and click "Fetch & Import URLs".

API Endpoints return a flat JSON array of URLs. They are saved and can be re-fetched anytime. They are never scanned — only their response URLs are.

Step 4: Collect Strings

Extract Strings tab → "Collect Strings". Jobs visit each URL internally — every __() call triggers the missing key handler which saves new strings to en.json.

Passive collection also works: Even without clicking "Collect Strings", any user visiting your site triggers collection for untranslated strings. The active scan is useful for initial setup or after major template changes.

Step 5: Translate

Translate tab → "Translate All Keys". Real-time per-language progress.

Step 6: Review & Edit

Translation Status tab → click a language → inline editor. Edit manually or click ✨ for AI translation.

Step 7: Monitor Missing Keys

Missing Keys tab → view untranslated strings from live traffic, grouped by locale with occurrence counts. Translate individually (✨) or batch per locale. See Missing Keys Dashboard for details.

How It Works

Standard Laravel __() Integration

The system works with Laravel's standard __() translation function — no custom Blade directives or proprietary syntax. This means:

  • Your templates stay clean and portable — standard {{ __('...') }} everywhere
  • No learning curve — if you know Laravel, you already know how to use this
  • Zero vendor lock-in — remove the package and your templates still work perfectly
  • IDE support works out of the box__() is recognized by all PHP IDEs

Passive Collection via handleMissingKeysUsing

Laravel's Translator class fires a callback whenever __() can't find a key. The TranslationServiceProvider hooks into this:

Lang::handleMissingKeysUsing(function ($key, $replace, $locale, $fallbackUsed) {
    $locale = $locale ?? app()->getLocale();
    $sourceLocale = config('translation.source_locale', 'en');

    // Filter empty keys and vendor package keys (package::group.key)
    if (trim($key) === '' || str_contains($key, '::')) {
        return $key;
    }

    if ($locale === $sourceLocale) {
        // Source language → add to en.json automatically
        $translations[$key] = $key;
        file_put_contents($path, ...);
    } else {
        // Target language → log to missing_translations table
        MissingTranslation::record($key, $locale);
    }
    return $key;
});

This means:

  • Source locale (en): Any __('new string') not in en.json gets added automatically
  • Target locales (ar, es): Missing translations logged to missing_translations table with occurrence counts
  • Zero overhead for existing translations: Callback only fires when a key is actually missing
  • Minimal filtering needed: Laravel resolves built-in keys (auth.*, validation.*, pagination.*) via its own lang files before the callback fires — only empty keys and vendor package keys are filtered

Active Scanning via URL Visits (on-demand)

When you click "Collect Strings" in the dashboard:

  1. Queue jobs visit each URL internally via app()->handle()
  2. The page renders — every __() call fires the translator
  3. The missing key handler catches new strings and adds them to en.json
  4. No HTML markers, no regex, no custom directives — just Laravel's own translator

Useful for: initial setup, after major template changes, or catching strings in unvisited pages.

URL Collection from API Endpoints

  1. User adds API endpoint URLs
  2. URLCollector saves with is_api = 1
  3. Endpoint fetched (internal for local URLs, HTTP for external)
  4. JSON array parsed → each URL saved as is_api = 0
  5. Duplicates skipped; endpoints re-fetchable anytime

Eloquent Models

TranslationUrl

use App\Models\Translate\TranslationUrl;

TranslationUrl::active();          // active = true
TranslationUrl::regularUrls();     // is_api = 0
TranslationUrl::apiEndpoints();    // is_api = 1
TranslationUrl::extractable();     // active AND is_api = 0

TranslationProgress

use App\Models\Translate\TranslationProgress;

$ext = TranslationProgress::stringExtraction()->first();
echo $ext->percentage;  // 75.5
echo $ext->status;      // 'running' | 'completed' | 'idle'

$p = TranslationProgress::translation()->forLocale('ar')->first();

MissingTranslation

use App\Models\Translate\MissingTranslation;

// Record a missing key (upsert — increments occurrences)
MissingTranslation::record('Hello World', 'ar');

// Get flat array of keys for a locale
$keys = MissingTranslation::keysForLocale('ar');

// Query with scopes
MissingTranslation::targetLocales()->recentFirst()->get();
MissingTranslation::forLocale('ar')->mostFrequent()->get();
MissingTranslation::since(now()->subDay())->get();

// Clear operations
MissingTranslation::clearLocale('ar');
MissingTranslation::clearAll();

Scopes: forLocale($locale), sourceLocale(), targetLocales(), recentFirst(), mostFrequent(), since($datetime)

API Endpoints for Dynamic URLs

// routes/api.php
Route::get('/sitemaps/products', function () {
    return Product::all()->map(
        fn($p) => url('/products/' . $p->slug)
    )->values();
});

Add in the dashboard → "Fetch & Import URLs". Use "Re-fetch All" to discover new content.

Local API detection: When api_scan_internal is enabled (default), local endpoints are fetched via internal Laravel requests to avoid php artisan serve deadlock.

Inline Translation Editor

  • Search strings by source or translation
  • Edit manually — saves on Enter or blur
  • AI-translate per string — click the ✨ icon
  • Green checkmark = translated, grey dash = missing

Missing Keys Dashboard

The Missing Keys tab provides real-time visibility into untranslated strings detected from live traffic:

  • Grouped by locale — see which languages have the most gaps at a glance
  • Occurrence tracking — know which strings are hit most frequently to prioritize
  • First/last seen timestamps — understand when strings appeared and how recent they are
  • Individual AI translate — click ✨ on any row, job dispatches immediately, row disappears
  • Bulk translate per locale — click "Translate All" on a locale group, progress bar appears
  • Processing banner — top-of-tab indicator when translations are in progress
  • Clear Resolved — removes entries that have been translated since they were logged
  • Clear All — wipes the entire missing_translations table
How it works: When a user visits /ar/about and the Arabic translation for "About Us" doesn't exist, the handleMissingKeysUsing hook logs it to missing_translations with locale ar. The occurrence counter increments on each hit.

Rate Limiting

'rate_limit_per_minute' => 300,
  • Free tier: 60–100 requests/minute
  • Paid tier: 300–3500 requests/minute

Troubleshooting

Strings Not Collected

# Verify the hook is active
php artisan tinker
>>> __('test string that does not exist')
# Check if it appeared in lang/en.json

# Enable logging
# 'log_process' => true in config/translation.php
# Check: storage/logs/laravel.log

Translations Not Working

php artisan tinker
>>> config('translation.translation.api_key')
php artisan queue:work --verbose
php artisan queue:failed

API Endpoints Not Importing

# Must return flat JSON array
curl https://your-app.com/api/sitemaps/blog
# ["https://...", "https://..."]

php artisan tinker
>>> \App\Models\Translate\TranslationUrl::apiEndpoints()->pluck('url')

Local API Endpoints Timing Out

If local API endpoints timeout with php artisan serve, ensure api_scan_internal is enabled:

// config/translation.php
'urls' => [
    'api_scan_internal' => true,
],

Missing Keys Not Appearing

  1. Verify TranslationServiceProvider is registered in bootstrap/providers.php
  2. Missing keys only log for target locales (e.g., /ar/about). Source locale keys go directly to en.json.
  3. Check the table:
    php artisan tinker
    >>> \App\Models\Translate\MissingTranslation::count()

API Reference

URLCollector Service

use App\Services\Translate\URLCollector;
$c = new URLCollector();

$c->addUrl('https://...');              // Single (null if dup)
$c->addBulk([...]);                    // Bulk, returns count
$c->addApiEndpoint('https://...');     // Save endpoint (is_api=1)
$c->collectFromApiEndpoint('...');    // Fetch + import
$c->collectFromApiEndpoints([...]);  // Multiple
$c->refreshAllApiEndpoints();        // Re-fetch all saved
$c->getExtractableUrls();            // Active non-API URLs
$c->removeById(42);
$c->toggleActive(42);
$c->clearRegularUrls();
$c->clearApiEndpoints();
$c->clearAll();

StringExtractor Service

$ext = new StringExtractor();

// Visit URL — triggers handleMissingKeysUsing for all __() calls
$ext->extractFromUrl('https://...');
// Returns void — the hook handles saving to en.json

$ext->getAllKeys('en');

MissingTranslation Model

use App\Models\Translate\MissingTranslation;

MissingTranslation::record('About Us', 'ar');    // Upsert with occurrence++
MissingTranslation::keysForLocale('ar');       // Flat array of keys
MissingTranslation::forLocale('ar');            // Scope
MissingTranslation::targetLocales();            // locale != source
MissingTranslation::recentFirst();              // ORDER BY last_seen DESC
MissingTranslation::mostFrequent();             // ORDER BY occurrences DESC
MissingTranslation::clearLocale('ar');
MissingTranslation::clearAll();

AITranslator Service

$t = new AITranslator();
$t->translate('Hello', 'ar');
$t->translateBatch([...], 'ar');
$t->getTargetLocales();
$t->isConfigured();