mindtwo/laravel-auto-translatable

AI-powered translation package for Laravel with smart markdown chunking and extensible adapters

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/mindtwo/laravel-auto-translatable

0.1.1 2026-01-05 19:48 UTC

This package is auto-updated.

Last update: 2026-01-05 19:48:49 UTC


README

mindtwo GmbH

Laravel Auto-Translatable

AI-powered translation package for Laravel with smart content chunking, extensible adapters, and automatic link replacement.


Table of Contents

Features

  • AI-Powered Translations - Uses any PRISM-supported provider (Anthropic, OpenAI, Google, etc.)
  • Smart Content Chunking - Intelligent chunking for long content:
    • Markdown: Respects document structure, never breaks mid-section
    • Plain Text: Chunks at paragraph, sentence, or word boundaries
    • Configurable: Set custom strategies per field
  • Automatic Link Replacement - Localize internal links in markdown content
  • Extensible Adapter System - Built-in support for popular i18n packages:
    • spatie/laravel-translatable
    • mindtwo/laravel-translatable
    • Custom adapters supported

Installation

Install via Composer:

composer require mindtwo/laravel-auto-translatable

Publish configuration and migrations:

php artisan vendor:publish --tag=auto-translatable-config
php artisan vendor:publish --tag=auto-translatable-migrations
php artisan migrate

Configuration

Configure the package in config/auto-translatable.php:

return [
    /*
    |--------------------------------------------------------------------------
    | AI Provider Configuration
    |--------------------------------------------------------------------------
    */
    'provider' => env('AUTO_TRANSLATABLE_PROVIDER', 'anthropic'),
    'model' => env('AUTO_TRANSLATABLE_MODEL', 'claude-3-5-sonnet-20241022'),

    /*
    |--------------------------------------------------------------------------
    | Token Configuration
    |--------------------------------------------------------------------------
    */
    'chunk_size' => env('AUTO_TRANSLATABLE_CHUNK_SIZE', 80000),
    'output_tokens' => env('AUTO_TRANSLATABLE_OUTPUT_TOKENS', 100000),

    /*
    |--------------------------------------------------------------------------
    | Locales
    |--------------------------------------------------------------------------
    */
    'default_source_locale' => env('AUTO_TRANSLATABLE_SOURCE_LOCALE', 'en'),
    'available_locales' => ['de', 'en', 'fr', 'es', 'it', 'pt', 'nl', 'pl'],

    /*
    |--------------------------------------------------------------------------
    | Queue Configuration
    |--------------------------------------------------------------------------
    */
    'queue_translations' => env('AUTO_TRANSLATABLE_QUEUE_ENABLED', true),
    'queue_connection' => env('AUTO_TRANSLATABLE_QUEUE_CONNECTION'),
    'queue_name' => env('AUTO_TRANSLATABLE_QUEUE_NAME', 'translations'),

    /*
    |--------------------------------------------------------------------------
    | Link Replacement
    |--------------------------------------------------------------------------
    */
    'link_replacement' => [
        'enabled' => false,
        'resolver' => null, // Class implementing LinkMappingResolver
        'internal_hosts' => [],
        'unmapped_links' => 'remove', // 'remove', 'keep', or 'warn'
    ],

    /*
    |--------------------------------------------------------------------------
    | Adapter & Auto-Apply
    |--------------------------------------------------------------------------
    */
    'adapter' => \Mindtwo\AutoTranslatable\Adapters\SpatieTranslatableAdapter::class,
    'auto_apply' => env('AUTO_TRANSLATABLE_AUTO_APPLY', false),
];

Token Configuration

The chunk_size and output_tokens configuration is critical for handling long content correctly:

  • chunk_size: Maximum input tokens per chunk (30-50% of model context window)
  • output_tokens: Maximum tokens for model generation
  • Rule: chunk_size + output_tokens ≤ model_context_window

Examples for different scenarios:

// Conservative (handles Chinese→German expansion ~2x)
// Context window: 200,000 tokens
['chunk_size' => 60000, 'output_tokens' => 120000]

// Balanced (most European language pairs)
// Context window: 200,000 tokens
['chunk_size' => 80000, 'output_tokens' => 100000]

// Aggressive (German→English, content shrinks ~20%)
// Context window: 200,000 tokens
['chunk_size' => 100000, 'output_tokens' => 80000]

Usage

Direct Translation

Translate any string directly using the TranslationService:

use Mindtwo\AutoTranslatable\Services\TranslationService;

class MyService
{
    public function __construct(
        private readonly TranslationService $translator,
    ) {}

    public function translateContent(): void
    {
        // Simple translation
        $result = $this->translator->translate(
            content: 'Hello World',
            sourceLocale: 'en',
            targetLocale: 'de'
        );

        echo $result->translated_content; // "Hallo Welt"
        echo $result->status; // TranslationStatus::COMPLETED
        echo $result->chunks_count; // 1
    }

    public function translateLongContent(): void
    {
        $longMarkdown = file_get_contents('article.md');

        // Automatically chunks large content
        $result = $this->translator->translate(
            content: $longMarkdown,
            sourceLocale: 'en',
            targetLocale: 'de',
            options: [
                'chunking_strategy' => 'markdown', // or 'plain', 'none', 'auto'
                'chunk_size' => 50000, // Override default
            ]
        );

        echo $result->chunks_count; // e.g., 3
        echo $result->translated_content; // Full translated content
    }
}

Model Translation

Translate Eloquent models automatically using the HasAutoTranslations trait.

1. Add Trait to Your Model

use Illuminate\Database\Eloquent\Model;
use Mindtwo\AutoTranslatable\Concerns\HasAutoTranslations;
use Spatie\Translatable\HasTranslations;

class Post extends Model
{
    use HasTranslations;      // From spatie/laravel-translatable
    use HasAutoTranslations;  // From this package

    public array $translatable = ['title', 'body', 'excerpt'];

    /**
     * Define which fields should be auto-translated.
     */
    public function autoTranslatableFields(): array
    {
        return ['title', 'body', 'excerpt'];
    }

    /**
     * Optional: Define chunking strategies per field.
     */
    public function chunkingStrategies(): array
    {
        return [
            'title' => 'none',      // Don't chunk titles
            'body' => 'markdown',   // Smart markdown chunking
            'excerpt' => 'plain',   // Plain text chunking
        ];
    }
}

2. Configure Adapter

Choose the appropriate adapter in your config:

// For spatie/laravel-translatable
'adapter' => \Mindtwo\AutoTranslatable\Adapters\SpatieTranslatableAdapter::class,

// For mindtwo/laravel-translatable
'adapter' => \Mindtwo\AutoTranslatable\Adapters\MindtwoTranslatableAdapter::class,

3. Translate Your Model

// Translate to all configured locales
$post->autoTranslate();

// Translations are queued by default
// Set 'queue_translations' => false in config for synchronous translation

// Later, retrieve translations:
$post->getTranslation('title', 'de');    // "Deutscher Titel"
$post->getTranslation('body', 'fr');     // "Corps français"

// Access translation results
$results = $post->translationResults()
    ->where('status', TranslationStatus::COMPLETED)
    ->get();

foreach ($results as $result) {
    echo "{$result->field_name}: {$result->target_locale} - {$result->status}";
}

Chunking Strategies

The package provides three chunking strategies:

1. Markdown Chunking (markdown)

Smart chunking that preserves document structure:

$result = $translator->translate(
    content: '# Heading\n\nParagraph...',
    sourceLocale: 'en',
    targetLocale: 'de',
    options: ['chunking_strategy' => 'markdown']
);

// Features:
// - Respects heading boundaries
// - Never breaks mid-paragraph
// - Preserves code blocks
// - Greedy packing of sections that fit together

2. Plain Text Chunking (plain)

Intelligent text chunking with fallback levels:

$result = $translator->translate(
    content: 'Long plain text...',
    sourceLocale: 'en',
    targetLocale: 'de',
    options: ['chunking_strategy' => 'plain']
);

// Chunking hierarchy:
// 1. Try paragraph boundaries (double newlines)
// 2. Fall back to sentence boundaries (. ! ?)
// 3. Last resort: word boundaries

3. No Chunking (none)

Pass content as-is without chunking:

$result = $translator->translate(
    content: 'Short title',
    sourceLocale: 'en',
    targetLocale: 'de',
    options: ['chunking_strategy' => 'none']
);

Custom Post-Processors

Add custom post-processing logic:

use Mindtwo\AutoTranslatable\Contracts\PostProcessor;
use Mindtwo\AutoTranslatable\Models\TranslationResult;

class CustomPostProcessor implements PostProcessor
{
    public function process(string $content, TranslationResult $result, array $context = []): string
    {
        // Your custom logic
        return str_replace('old', 'new', $content);
    }
}

// Use it
$result = $translator->translate(
    content: 'Some content',
    sourceLocale: 'en',
    targetLocale: 'de',
    options: [
        'post_processors' => [
            new CustomPostProcessor(),
        ],
    ]
);

Link Replacement

Automatically localize internal links in markdown content.

1. Create a Link Mapping Resolver

namespace App\Services;

use Mindtwo\AutoTranslatable\Contracts\LinkMappingResolver;

class BlogLinkResolver implements LinkMappingResolver
{
    /**
     * Provide static URL mappings.
     */
    public function getMapping(string $sourceLocale, string $targetLocale): array
    {
        return [
            '/blog/getting-started' => '/blog/erste-schritte',
            '/contact' => '/kontakt',
            '/about' => '/ueber-uns',
        ];
    }

    /**
     * Dynamic resolution for URLs not in static mapping.
     */
    public function resolve(string $url, string $sourceLocale, string $targetLocale): ?string
    {
        // Example: Fetch from database
        $post = Post::query()->where('slug', ltrim($url, '/'))->first();

        if ($post) {
            return '/' . $post->getTranslation('slug', $targetLocale);
        }

        return null;
    }
}

2. Configure Link Replacement

'link_replacement' => [
    'enabled' => true,
    'resolver' => \App\Services\BlogLinkResolver::class,
    'internal_hosts' => [
        'https://example.com',
        'https://www.example.com',
    ],
    'unmapped_links' => 'remove', // Options: 'remove', 'keep', 'warn'
],

3. Example

Input (English):

Read our [getting started guide](/blog/getting-started) and [contact us](/contact).
Also check out [this untranslated post](/blog/new-post).
Visit [Google](https://google.com) for more info.

Output (German):

Read our [getting started guide](/blog/erste-schritte) and [contact us](/kontakt).
Also check out this untranslated post.
Visit [Google](https://google.com) for more info.

Events

The package dispatches events throughout the translation lifecycle:

TranslationCompleted

Fired when a single field translation completes successfully.

use Mindtwo\AutoTranslatable\Events\TranslationCompleted;

Event::listen(TranslationCompleted::class, function (TranslationCompleted $event) {
    // $event->result - TranslationResult instance
    // $event->model - Model that was translated (if applicable)
    // $event->field - Field name that was translated

    Log::info("Translation completed: {$event->field} -> {$event->result->target_locale}");
});

TranslationFailed

Fired when a translation fails.

use Mindtwo\AutoTranslatable\Events\TranslationFailed;

Event::listen(TranslationFailed::class, function (TranslationFailed $event) {
    // $event->result - TranslationResult instance
    // $event->error - Error message
    // $event->model - Model that failed translation
    // $event->field - Field name that failed

    Log::error("Translation failed: {$event->field} - {$event->error}");

    // Optionally notify administrators
    Mail::to('admin@example.com')->send(new TranslationFailedNotification($event));
});

ModelTranslationCompleted

Fired when all fields of a model have been processed (some may have failed).

use Mindtwo\AutoTranslatable\Events\ModelTranslationCompleted;

Event::listen(ModelTranslationCompleted::class, function (ModelTranslationCompleted $event) {
    // $event->model - The translated model
    // $event->results - Collection of TranslationResult instances
    // $event->fields - Array of field names that were translated

    $successful = $event->results->where('status', TranslationStatus::COMPLETED)->count();
    $failed = $event->results->where('status', TranslationStatus::FAILED)->count();

    Log::info("Model translation completed: {$successful} successful, {$failed} failed");
});

Manual Translation Application

Listen to events and apply translations manually:

First, disable auto applying:

AUTO_TRANSLATABLE_AUTO_APPLY=false
use Mindtwo\AutoTranslatable\Events\ModelTranslationCompleted;
use Mindtwo\AutoTranslatable\Contracts\TranslatableAdapter;
use Mindtwo\AutoTranslatable\Adapters\SpatieTranslatableAdapter;

Event::listen(ModelTranslationCompleted::class, function (ModelTranslationCompleted $event) {
    // Review changes manually, e.g. through an admin interface, then:
    resolve(SpatieTranslatableAdapter::class)->applyTranslations($event->model, $event->results);
});

Advanced Usage

Custom Adapters

Create a custom adapter for your i18n package:

namespace App\Services;

use Illuminate\Database\Eloquent\Model;
use Mindtwo\AutoTranslatable\Contracts\TranslatableAdapter;

class MyCustomAdapter implements TranslatableAdapter
{
    public function supports(Model $model): bool
    {
        return $model instanceof MyTranslatableModel;
    }

    public function getAvailableLocales(Model $model): array
    {
        return config('app.locales');
    }

    public function getSourceLocale(Model $model): string
    {
        return $model->source_locale ?? config('app.locale');
    }

    public function getFieldValue(Model $model, string $field, string $locale): ?string
    {
        return $model->translate($field, $locale);
    }

    public function applyTranslations(Model $model, string $locale, array $translations): void
    {
        foreach ($translations as $field => $value) {
            $model->setTranslation($field, $locale, $value);
        }
        $model->save();
    }
}

Register your adapter:

'adapter' => \App\Services\MyCustomAdapter::class,

License

MIT License. See LICENSE for details.

Credits

Created by mindtwo GmbH

Support