Open source April 2025
Laratext

Laratext

Laratext

Laratext names a string with a stable key and a readable default in the same call, then fills in the other languages for you from a scan of your source.

Installation

composer require edulazaro/laratext
php artisan vendor:publish --tag="texts"

That publishes config/texts.php, where you list the languages you support and the service that translates them.

Naming a string

The core is one helper. You give it a key and the readable text as a default, and it returns the translation for the current locale, falling back to your default when there is not one yet.

text('auth.failed', 'These credentials do not match our records.');

In Blade the same thing is a directive.

@text('auth.failed', 'These credentials do not match our records.')

The key is stable, so you can reword the English freely. The sentence sits in the template, so you read it without opening a language file.

If you pass only a key with no default, the string still resolves: the scan turns a key like contact_us into a readable "Contact Us".

Placeholders

Placeholders use Laravel's :name syntax and take a replacement array. They survive translation, so :name still means :name in Spanish.

text('welcome.user', 'Welcome, :name!', ['name' => $user->name]);

Configuring languages and the translator

In config/texts.php you declare which languages you support and which service translates them.

'default_translator' => 'openai',

'translators' => [
    'openai' => EduLazaro\Laratext\Translators\OpenAITranslator::class,
    'google' => EduLazaro\Laratext\Translators\GoogleTranslator::class,
    'claude' => EduLazaro\Laratext\Translators\ClaudeTranslator::class,
],

'languages' => [
    'en' => 'English',
    'es' => 'Spanish',
    'fr' => 'French',
],

OpenAI, Google and Claude ship with the package. The Claude translator runs through the Messages API with prompt caching, so repeated batches within a single scan reuse the cached instructions.

The scan command

The scan walks your PHP and Blade files, collects every text() and @text key with its default, works out which are missing from each language, translates only those, and writes them into lang/es.json, lang/fr.json and so on.

php artisan laratext:scan --write

The base language is your app.locale; the targets are the languages in the config.

Option What it does
--dry Preview without writing
--diff Show what would change
--lang=es Restrict to one language
--translator=google Override the configured service
--resync Retranslate keys whose source text actually changed, not only the missing ones

Writing your own translator

The translation service is an interface, so you can point it at anything: a different API, a glossary-aware endpoint, a human review queue.

php artisan make:translator DeepLTranslator

That scaffolds a class extending the package's Translator base and implementing TranslatorInterface. You implement one method, which translates a single string into every target language and returns them keyed by language code.

namespace App\Translators;

use EduLazaro\Laratext\Contracts\TranslatorInterface;
use EduLazaro\Laratext\Translator;

class DeepLTranslator extends Translator implements TranslatorInterface
{
    public function translate(string $text, string $from, array $to): array
    {
        $results = [];

        foreach ($to as $language) {
            $results[$language] = $this->callDeepL($text, $from, $language);
        }

        return $results;
    }
}

Extending the base Translator hands you batchTranslate, which splits a large set of strings into safe batches capped by character count and item count.

If your API accepts many strings per call, override translateMany to translate a whole batch in one request instead of one call per string. That is the difference between one round trip and a hundred.

Register the class in the translators map in config/texts.php and select it with --translator, or set it as the default.

built and maintained by Edu Lazaro · MIT license