Laravel langsys/langsys-php-laravel
A thin Laravel binding over langsys/langsys-php: the @t Blade directive, the t() helper, and locale middleware. The phrase in your code is both the lookup key and the base-language default — no keys file, no extraction step. Translation happens on the server, so the page arrives already localized.
Backend SDK, so no in-browser playground on this page — the browser can't run PHP. The demo app is the live version of every example below: clone it and php artisan serve, or open it in a Codespace — a real PHP container that installs and serves it for you. Full package reference, including automatic page translation and Inertia seeding, lives in the Laravel SDK docs.
Using an AI coding agent? Give it this prompt
Add Langsys translation to this Laravel app.
1. Read https://docs.langsys.dev/llms.txt, then follow
https://docs.langsys.dev/learn/sdk/laravel/ for the install and setup.
2. The English text in the code is the translation key. Wrap user-facing
strings as they are: no keys file, no extraction step, and no
hand-written plural or gender branches (Langsys generates those).
3. Read the Langsys project id and API key from environment variables.
The write key is for development and servers only; anything shipped
to the browser gets the read-only key.
4. Keep brand names and user-generated content out of translation. Install
composer require langsys/langsys-php-laravel
php artisan vendor:publish --tag=langsys-config # The server can hold a WRITE key — unlike a browser app, it's never
# shipped to visitors. A write key is what lets new phrases register.
LANGSYS_API_KEY=your-api-key
LANGSYS_PROJECT_ID=your-project-id No project or keys yet? The Quickstart gets you both in two minutes.
Middleware
Two pieces of middleware do the framework work: DetectLocale resolves the visitor's locale (query param, cookie, session, then Accept-Language) and persists their choice; FlushPendingRegistrations sends newly discovered phrases to Langsys after the response goes out, so registration never slows a page down.
// bootstrap/app.php (Laravel 11/12)
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
// Resolves ?locale= / cookie / session / Accept-Language,
// then persists the visitor's choice.
\Langsys\Laravel\Http\Middleware\DetectLocale::class,
// After the response is sent, registers any phrases
// rendered for the first time.
\Langsys\Laravel\Http\Middleware\FlushPendingRegistrations::class,
]);
}) Translate in Blade
One directive: @t(phrase, category?, params?, locale?). Params interpolate with {name} markers — numbers and dates come out locale-formatted — and the output is HTML-escaped.
<h1>@t('Welcome to our store', 'Home')</h1>
<p>@t('Hello, {name}! You have {count} new messages.', 'Greetings',
['name' => $name, 'count' => $count])</p> Anywhere in PHP
@t is just the t() helper with escaping — the helper itself works anywhere: controllers, Livewire components, jobs, mail, validation messages.
// Controllers, Livewire components, jobs, mail — anywhere:
$title = t('Order confirmed', 'Checkout');
$body = t('Your order {id} ships on {date}.', 'Checkout', [
'id' => (string) $order->number, // strings skip number formatting
'date' => $order->ships_at, // DateTime → locale-formatted
]); Plurals
Write the sentence flat: Langsys generates each locale's plural forms during translation — the base locale included — and the SDK renders the branch that matches your count (full ICU via ext-intl). Same in every SDK, so phrases stay portable between your backend and frontend.
// Written flat. Langsys generates each locale's plural forms during
// translation (the base locale included) and the SDK picks the branch.
t('You have {count} items in your cart', 'Cart', ['count' => $count]); The plural forms Langsys generates reach PHP from langsys/langsys-php 1.3.2; earlier versions render the plural form for every count, including 1.
Gender agreement
Spanish, French, or Russian inflect {username} has been invited for the person it’s about. Langsys generates those branches during translation — the same way it generates plurals — and the argument is named after the variable, with _gender appended: male, female, or other. Pass it whenever you know it. How it works, and how to find the phrases that need it →
{{-- Pass the agreement argument alongside the value it inflects. --}}
@t('{username} has been invited', 'Team', [
'username' => $post->author->name,
'username_gender' => $post->author->gender, // 'male' | 'female' | 'other'
]) Don’t know the gender? Pass 'other' explicitly — it’s the neutral branch. If it is missing anyway, the base SDK takes the other branch for you (langsys/langsys-php 1.3.1 and later, with or without ext-intl), so the sentence still reads correctly.
Categories
The category scopes a phrase, so the same words can translate differently in different contexts.
{{-- Same word, two meanings — the category disambiguates. --}}
<strong>@t('Home', 'Main Menu')</strong> {{-- "Inicio" --}}
<strong>@t('Home', 'Home repairs')</strong> {{-- "Casa" --}} One marker style: {name}. In the JS SDKs, markup params use %name% because single braces collide with JSX and Vue template syntax. Blade has no such collision, so PHP-land uses the canonical {name} form everywhere — the same form t() uses in every SDK.
Which one do I use?
| You’re translating… | Use | Why |
|---|---|---|
| Text in a Blade template. | @t(...) | The t() helper plus HTML escaping — safe for direct template output. |
| A string anywhere else — controllers, Livewire, jobs, mail. | t() | Returns a plain string with params interpolated and ICU plurals resolved. |
| Something that must never translate — brand names, code, IDs. | leave it unwrapped | Coverage is explicit: only strings you wrap are translated; untagged output passes through verbatim. |
| A whole HTML document you already have as a string. | Langsys::client()->translatePage() | Drops to the raw PHP SDK's full-page pipeline — title, meta, and body text in one call. |
Rule of thumb: Blade → @t · PHP → t() · untouchable → don’t wrap it.
Token discovery on the server
Render a phrase that Langsys hasn't seen and it's queued, then registered after the response terminates — the visitor sees your base-language text immediately, and the phrase appears in your dashboard ready to translate. This needs a write key; on a read-only key the queue is skipped silently, which is exactly what you want in environments that shouldn't mutate the catalog.
Pairing with the JS SDKs
Running Inertia or a hybrid app? Hand the JS SDKs the catalog the server already fetched, so the client doesn't fetch it again — the same initialTranslations handoff the frontend SDK pages describe.
// app/Http/Middleware/HandleInertiaRequests.php
use Langsys\Laravel\Support\InertiaSsrProps;
public function share(Request $request): array
{
return [...parent::share($request), ...InertiaSsrProps::share()];
} // resources/js — Vue shown; same shape for React/Svelte
import { LangsysApp, useLocaleStore } from 'langsys-js-vue';
const { store } = useLocaleStore(props.langsys.initialTranslationsLocale);
LangsysApp.init({
projectid: import.meta.env.VITE_LANGSYS_PROJECT_ID,
key: import.meta.env.VITE_LANGSYS_API_KEY, // read-only key on the client
UserLocaleStore: store,
initialTranslations: props.langsys.initialTranslations,
initialTranslationsLocale: props.langsys.initialTranslationsLocale,
}); The facade & the raw client
The Langsys facade mirrors t(), and Langsys::client() hands you the underlying langsys/langsys-php client for everything beyond phrase lookup — catalog reads, cache control, content blocks.
use Langsys\Laravel\Facades\Langsys;
Langsys::translate('Save', 'UI'); // same seam as t()
Langsys::client()->getTranslations('es-es'); // the vanilla langsys/langsys-php client
Langsys::client()->clearCache('es-es'); // pick up dashboard edits before the TTL