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

Querying and search

Query scopes

HasTranslations adds three JSON-path scopes that work on any Laravel-supported database. The locale defaults to the app locale:

Topic::query()->whereLocale('name', 'Investing', 'en')->get(); // exact per-locale value (locale defaults to app locale)
Topic::query()->whereHasLocale('name', 'sk')->get();           // rows with a non-blank 'sk' value
Topic::query()->whereMissingLocale('name', 'sk')->get();       // rows missing 'sk' (feed an AI-fill queue)

Each scope checks that the field is translatable (NotATranslatableAttributeException otherwise) and validates the locale format (InvalidLocaleException), so a request-supplied locale never reaches the JSON-path expression unchecked. whereMissingLocale() pairs naturally with a fill queue:

Topic::query()
    ->whereMissingLocale('description', 'sk')
    ->each(fn (Topic $topic) => FillMissingTranslations::dispatch($topic, 'description'));

Search across locales

Translatable::search() adds a per-locale, case-insensitive search across one or more fields and every supported locale — ilike on PostgreSQL, a lowercased like everywhere else:

use RoundlyConsulting\Translatable\DataTransferObjects\TranslationSearch;
use RoundlyConsulting\Translatable\Facades\Translatable;

$topics = Translatable::search(
    Topic::query(),
    new TranslationSearch(fields: ['name', 'description'], term: 'invest'),
)->get();

// The term is a literal substring — this matches "100%", not every row:
Translatable::search(Topic::query(), new TranslationSearch(fields: ['name'], term: '100%'))->get();

The term is treated as a literal substring: %, _ and \ are escaped and the SQL states its escape character explicitly, so a search for 100% or a_b finds exactly those rows on every driver — including SQLite and SQL Server, which have no default LIKE escape character. The term is always bound; only the searched field name, validated as a plain identifier, reaches the SQL. A search with no fields (or no supported locale) searches nothing and matches no rows — never every row.

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.