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,
    'translatorio' => EduLazaro\Laratext\Translators\TranslatorioTranslator::class,
],

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

Four translators ship with the package. Pick one for a scan with --translator=, or make it the default with default_translator.

Translator --translator= Key in .env What it is
OpenAI openai OPENAI_API_KEY OpenAI's chat models, gpt-5.4-nano unless OPENAI_MODEL says otherwise
Claude claude ANTHROPIC_API_KEY Anthropic's Messages API, claude-haiku-4-5 unless ANTHROPIC_MODEL says otherwise
Google Translate google GOOGLE_TRANSLATOR_API_KEY The Google Cloud Translation API
Translatorio translatorio TRANSLATORIO_API_KEY A translation API that protects placeholders and tags itself

The Claude translator runs with prompt caching, so repeated batches within a single scan reuse the cached instructions. context in the same file is a sentence about your application (what it does, the register it uses, the names it never translates) sent with every batch. Google Translate takes no prompt, so it ignores it; the other three read it.

What each model can do

Translating with gpt-3.5-turbo is not translating with gpt-5.4-nano: one answers 4,096 tokens at most and the other 128,000, one takes temperature and another refuses any request that sends it. The OpenAI and Claude translators know the models of both providers:

  • Batches are sized by the answer they produce: a batch into twenty languages answers twenty times its size, so it is cut to fit one answer of the model, and languages go in groups when one string into all of them would not fit.
  • An answer cut short at the limit is split in two, never sent again whole.
  • Each model gets only the parameters it accepts: temperature where it is taken, reasoning_effort for reasoning models, a JSON schema where Structured Outputs exist.
  • The source language is never sent, and pt_BR or zh_CN are written under the code you use.

A model the package does not know is asked the way every model was before. Correct one or add a new one under models in config/texts.php:

'models' => [
    'gpt-6' => ['max_output_tokens' => 128000, 'temperature' => false, 'json' => 'schema', 'reasoning' => true, 'reasoning_effort' => 'low'],
],

Translating with Translatorio

Translatorio is a translation API that takes placeholders (:name, , %s), HTML tags and links out of each string before the model reads it and puts them back afterwards, then says whether every one came back. The scan prints a warning for a key whose placeholder did not survive instead of writing a broken string.

# .env
TRANSLATORIO_API_KEY=tr_live_...
TRANSLATORIO_GLOSSARY=shop        # optional: one of your glossaries
TRANSLATORIO_FORMALITY=informal   # optional
php artisan laratext:scan --write --translator=translatorio

Laravel codes such as pt_BR are written back under the same code, more than twenty languages go in several calls, and the context of config/texts.php travels with every batch. Retries reuse the same idempotency key, so a retried batch is never charged twice. Running out of credits halfway keeps what was already translated, and the next scan finishes the rest. A tr_test_ key runs the whole scan for free and writes marked copies, like [es] Welcome, without reaching the model.

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=translatorio 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