NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

No fake, by design

The Translatable facade has no fake(). Nothing on it writes to the database, queues, mails, dispatches an event or calls out, so there is nothing for a fake to record. To pin behaviour in a test, set the config — or swap in a TranslationManager subclass to override one method; facade calls and every model observe it (see DI and actions):

use RoundlyConsulting\Translatable\Enums\FallbackMode;
use RoundlyConsulting\Translatable\Facades\Translatable;
use RoundlyConsulting\Translatable\Support\TranslationManager;

config()->set('translatable.locales', ['en', 'de']);        // the locale set
config()->set('translatable.fallback', FallbackMode::None); // the fallback chain

// or override one method — facade calls and every model observe the subclass:
Translatable::swap(new class extends TranslationManager {
    public function supported(): array { return ['en', 'de']; }
});

Testing your models

Translations live in an ordinary column, so tests run against real models: set the app locale and config per test, then assert through the public API. The examples assume the default en/sk locales and a Topic model with name fillable; the event test also needs DispatchesTranslationEvents:

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Translatable\Enums\FallbackMode;
use RoundlyConsulting\Translatable\Events\TranslationsChanged;

it('falls back to English for an untranslated locale', function () {
    app()->setLocale('sk');

    $topic = new Topic(['name' => ['en' => 'Investing']]);

    expect($topic->name)->toBe('Investing')
        ->and($topic->translatedOrNull('name'))->toBeNull()
        ->and($topic->missingLocales('name'))->toBe(['sk']);
});

it('renders nothing for a missing locale in None mode', function () {
    config()->set('translatable.fallback', FallbackMode::None);
    app()->setLocale('sk');

    $topic = new Topic(['name' => ['en' => 'Investing']]);

    expect($topic->name)->toBeNull();
});

it('dispatches TranslationsChanged on create', function () {
    Event::fake([TranslationsChanged::class]);

    $topic = Topic::create(['name' => ['en' => 'Investing']]);

    Event::assertDispatched(TranslationsChanged::class, fn (TranslationsChanged $event): bool =>
        $event->model->is($topic) && $event->changedAttributes === ['name']);
});

it('shows the missing-locales badge', function () {
    $topic = new Topic(['name' => ['en' => 'Investing']]);

    $html = Blade::render('<x-translatable-status :model="$model" />', ['model' => $topic]);

    expect($html)->toContain('translatable-status--incomplete');
});

The package’s own suite

The suite runs on SQLite by default. Point it at a real engine with the standard TESTING_DB_* variables — TESTING_DB_DRIVER is the one that actually moves it:

composer test

TESTING_DB_DRIVER=pgsql TESTING_DB_HOST=127.0.0.1 TESTING_DB_PORT=5432 \
  TESTING_DB_DATABASE=testing TESTING_DB_USERNAME=testing TESTING_DB_PASSWORD=secret \
  vendor/bin/pest

CI runs all three engines: SQLite catches a LIKE without an ESCAPE clause, PostgreSQL covers the ilike path and real jsonb columns (including sluggable’s per-locale slugs over them), and MySQL proves the like branch on a real server. PostgreSQL-only cases skip visibly when no engine is reachable, so a run that asserts nothing can’t report green.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.