PHP langsys/langsys-php

The base PHP SDK: one client for any PHP app — plain PHP, a framework, a worker or a CLI. 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. On Laravel? The Laravel package wraps this client with a Blade directive, middleware and a facade. Full reference: the README.

Using an AI coding agent? Give it this prompt
Add Langsys translation to this PHP app.

1. Read https://docs.langsys.dev/llms.txt, then follow
   https://docs.langsys.dev/learn/sdk/php/ 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

PHP 7.4 or later, with the curl, json and intl extensions — intl is what renders plurals and formats numbers and dates for each locale.

.env
# A server can hold a WRITE key — it never reaches visitors. A write key is
# what lets phrases your pages use for the first time register themselves.
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.

The client

One client per request. It reads its credentials from the environment and caches each locale's catalog, so translating a page costs one API call at most.

View source ↗
<?php
require 'vendor/autoload.php';

use LangsysSDKClient;

// Reads LANGSYS_API_KEY and LANGSYS_PROJECT_ID from the environment.
$langsys = new Client();
$langsys->setLocale('es-es');

Translate

translate() takes the locale second and the category third. Pass null for the locale to use the client's current one.

View source ↗
// translate($phrase, $locale, $category, $contentBlockId, $params)
echo $langsys->translate('Order confirmed', null, 'Checkout');   // current locale
echo $langsys->translate('Order confirmed', 'fr-fr', 'Checkout'); // an explicit one

// A phrase the project doesn't have yet renders as written — never blank.

Values in a sentence

Placeholders use {name}, the same form every Langsys SDK stores, so one catalog renders identically from a PHP backend and a JS frontend.

View source ↗
// Pass values as params, never build the string yourself.
echo $langsys->translate('Hello, {name}!', null, 'Greetings', null, ['name' => $user->name]);

// sprintf('Hello, %s!', $name) would register "Hello, Sarah!", "Hello, Ahmed!", …
// — one catalog entry per value, billed as words, and impossible to undo.

Plurals

Watch · 1:14 Pluralization

Write the sentence flat: Langsys generates each locale's plural forms during translation, and the SDK renders the branch that matches your count.

View source ↗
// Written flat. Langsys generates each locale's plural forms during
// translation and the SDK picks the branch that matches the count.
echo $langsys->translate('You have {count} new messages.', null, 'Inbox', null, ['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 →

Passing the gender argument View source ↗
// Pass the agreement argument alongside the value it inflects.
echo $langsys->translate('{username} has been invited', null, 'Team', null, [
    'username'        => $user->name,
    'username_gender' => $user->gender, // 'male' | 'female' | 'other'
]);

Don’t know the gender? Pass 'other' explicitly — it’s the neutral branch. If it is missing anyway, the SDK takes the other branch for you, 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.
echo $langsys->translate('Home', null, 'Main Menu');     // es: "Inicio"
echo $langsys->translate('Home', null, 'Home repairs');  // es: "Casa"

The visitor's locale

Set it from your own routing or the user's settings, or let the client read the browser's Accept-Language header.

View source ↗
use LangsysSDKLocaleLocaleDetector;

// Explicit, from your own routing or the user's settings:
$langsys->setLocale('pt-br');

// Or from the browser: without setLocale(), getLocale() reads Accept-Language
// and falls back to the project's base locale.
$locale = $langsys->getLocale();

// The same detection, from a header your framework hands you:
$locale = LocaleDetector::fromAcceptLanguage($request->getHeaderLine('Accept-Language'));

In your templates

A small helper gives your views a short name and escapes the output.

helpers.php
<?php
// helpers.php — one client per request, one short name in your templates.
use LangsysSDKClient;

function t(string $phrase, string $category, array $params = []): string
{
    static $langsys = null;
    $langsys ??= (new Client())->setLocale($_SESSION['locale'] ?? 'en-us');

    return htmlspecialchars($langsys->translate($phrase, null, $category, null, $params));
}

// In a template:
// <h1><?= t('Welcome to our store', 'Home') ?></h1>

Token discovery on the server

With a write key, every phrase the project hasn't seen is queued as you render it and registered after the response — the visitor sees your base-language text immediately, and the phrase is translated for the next one. With a read-only key nothing is registered and nothing breaks.

View source ↗
// With a write key, translate() queues every phrase the project doesn't have
// and sends them after the response, from a shutdown handler. Nothing to call.
// With a read key the queue is simply dropped — no errors.

// Or register up front, e.g. from a deploy script:
$langsys->registerPhrases([
    ['phrase' => 'Order confirmed', 'category' => 'Checkout'],
    ['phrase' => 'Copyright 2026', 'category' => 'Footer', 'translatable' => false],
]);

Whole pages

Already have the page as an HTML string? Translate it in one pass, head included.

View source ↗
// An HTML page you already have as a string: <title>, meta and OpenGraph
// tags, and the body — phrases and content blocks — in one pass.
ob_start();
// … your app renders the page …
$html = ob_get_clean();

echo $langsys->setLocale('es-es')->translatePage($html, 'Home');

Caching

View source ↗
// Catalogs are cached for an hour (a file cache by default; Redis or none
// via LANGSYS_CACHE_DRIVER). After editing translations in the dashboard:
$langsys->clearCache('es-es');
Search the docs & Learning Center