PHP client for LibreTranslate and LTEngine translation APIs with synchronous and asynchronous support.
Forked from jefs42/libretranslate, refactored for PHP 8.2+ with Guzzle HTTP client and async batch translations via Guzzle Promises.
When translating the same content into multiple languages (e.g. en, et, ru, lt, lv, fi), sequential requests are slow. LTEngine uses vLLM with continuous batching, but consumer GPUs still need bounded concurrency. This client keeps async batches efficient while respecting server overload signals such as 429 Retry-After.
| Mode | Method(s) | Blocks caller? | Best for |
|---|---|---|---|
| Sync single | translate() |
Yes — one blocking POST | One-off translates, admin forms |
| Sync multi | translate(['a','b',...]) |
Yes — one blocking POST with array payload | Small fixed sets |
| Async batch | translateBatch() |
Yes — blocks until all done | Many texts, no per-result side effects |
| Pipelined async | startAsyncBatch() + pump() + resolveAsyncBatch() |
Only at resolve() |
Maximum throughput — keeps GPU busy during DB I/O |
startAsyncBatch() parks each request inside a deferred then() chain on Guzzle's global task queue. Until something calls PromiseUtils::queue()->run() (which wait() does internally), those callbacks have not fired — so requestAsync() has not even registered the cURL handle yet. Meanwhile responses already in the kernel buffer are not read because nobody is calling curl_multi_exec.
Result: LTEngine GPU goes idle between batches.
pump() fixes both in one non-blocking call:
PromiseUtils::queue()->run(); // fire deferred then() → register cURL handles
$multiHandler->tick(); // step curl_multi once → send data, read responses
With the default selectTimeout=0.0 the tick is fully non-blocking — safe to call from a save callback or a tight loop.
| Issue | Fix |
|---|---|
| #6 Replace raw cURL | Guzzle ^7.0 as HTTP backend (enables Promises for async) |
| #7 PascalCase method names | camelCase methods: translate(), detect(), languages(), settings() |
| #9 Multi-mode translation broken | Array input sent directly as JSON without urlencode corruption |
#10 detect() returns only first result |
Returns complete detection results array with confidence scores |
Additional changes:
- PHP 8.2+ with strict types, typed properties, union types
- Dual authentication: API key parameter + Authorization header (Basic/Bearer)
- Format auto-detection:
"html"or"text"inferred per-call from text content, overridable via constructor,setFormat(), or per-call parameter - Async batch translation:
translateBatch(),translateMultiTarget(),startAsyncBatch()/resolveAsyncBatch()for pipelined throughput - Removed
ltmanagefunctionality (not relevant for remote API usage) - Removed
translateFiles()andsuggest()(not supported by LTEngine)
- PHP 8.2+
- ext-json
- ext-curl (required by Guzzle)
composer require afanasjev82/libretranslate-phpuse Afanasjev82\LibretranslatePhp\LibreTranslate;
$translator = new LibreTranslate("https://your-server.com", 9453);
# With API key (LibreTranslate standard)
$translator->setApiKey("your-api-key");
# Or with Authorization header (Basic/Bearer)
$translator->setAuth("Basic", "base64encodedcredentials");
# Translate (format auto-detected: "<p>..." → html, plain text → text)
$result = $translator->translate("Hello world", "en", "et");
echo $result; # "Tere maailm"
# Translate an HTML snippet — format auto-detected as "html"
$result = $translator->translate("<p>Hello</p>", "en", "et");
# Translate with default languages
$translator->setLanguages("en", "et");
$result = $translator->translate("Hello world");
# Detect language (returns full results array)
$detections = $translator->detect("Tere maailm");
# [{"language": "et", "confidence": 95.2}, ...]
# Get available languages
$languages = $translator->languages();
# ["en" => "English", "et" => "Estonian", ...]
# Get server settings
$settings = $translator->settings();All translate methods auto-detect the content format from the text: an HTML opening tag anywhere in the string triggers "html", everything else is "text". Override at any level:
# Set a project-wide default (overrides auto-detect for every call)
$translator->setFormat("html");
# Per-call override (highest priority — overrides both default and auto-detect)
$translator->translate("<p>Hello</p>", "en", "et", "text");
# Via constructor — 6th argument, after guzzleOptions
$translator = new LibreTranslate("https://your-server.com", 9453, "en", "et", [], "html");
# Reset to auto-detect
$translator->setFormat(null);Priority chain: explicit call param → setFormat() default → auto-detect from text
Auto-detection adds a single regex match per call — negligible overhead (microseconds) compared to HTTP roundtrip time.
use Afanasjev82\LibretranslatePhp\AsyncLibreTranslate;
$translator = new AsyncLibreTranslate("https://your-server.com", 9453);
$translator->setAuth("Basic", "base64encodedcredentials");
$translator->setMaxConcurrentRequests(4); # Good starting point for a single RTX 3090
$translator->setRetryPolicy(2, 2); # Retry transient overloads using Retry-After when present
# Translate one text into 6 languages at once — dispatched with bounded concurrency
$results = $translator->translateMultiTarget(
"Product description here",
["en", "et", "ru", "lt", "lv", "fi"],
"auto",
);
# Returns: ["en" => "...", "et" => "...", "ru" => "...", ...]
foreach ($results as $lang => $translatedText) {
echo $lang . ": " . $translatedText . "\n";
}# Translate different texts/language pairs concurrently
$batch = [
["text" => "Hello world", "source" => "en", "target" => "et"],
["text" => "Bonjour le monde", "source" => "fr", "target" => "en"],
["text" => "Tere maailm", "source" => "et", "target" => "ru"],
];
$results = $translator->translateBatch($batch);
# All translations returned — the client keeps only a bounded number in flight
foreach ($results as $index => $translatedText) {
echo $batch[$index]["target"] . ": " . $translatedText . "\n";
}$promise = $translator->translateAsync("Hello world", "en", "et");
# Do other work while request is in flight...
$result = $promise->wait(); # Block when ready
echo $result; # "Tere maailm"startAsyncBatch() dispatches requests without blocking. resolveAsyncBatch() blocks when you need the results. Call pump() between the two to keep LTEngine busy while your code does I/O (database writes, file saves, etc.):
use Afanasjev82\LibretranslatePhp\AsyncLibreTranslate;
$translator = new AsyncLibreTranslate("https://your-server.com", 9453);
$translator->setAuth("Basic", "base64encodedcredentials");
$translator->setMaxConcurrentRequests(4);
# selectTimeout defaults to 0.0 s — pump() is non-blocking out of the box.
# Restore Guzzle default (1.0 s) if you need a sleeping event loop instead:
# $translator->setSelectTimeout(1.0);
$batches = array_chunk($items, 20);
$pendingPromises = null;
$pendingBatch = null;
foreach ($batches as $batch) {
$batchItems = array_map(fn($item) => [
"text" => $item["text"],
"source" => "auto",
"target" => $item["lang"],
], $batch);
# Dispatch next batch — non-blocking, requests start flowing immediately
$newPromises = $translator->startAsyncBatch($batchItems);
# Save previous batch to DB while new requests are in flight
if ($pendingPromises !== null) {
$results = $translator->resolveAsyncBatch($pendingPromises);
foreach ($pendingBatch as $index => $item) {
$db->save($item["id"], $results[$index]);
$translator->pump(); # keep LTEngine busy while we write to DB
}
}
$pendingPromises = $newPromises;
$pendingBatch = $batch;
}
# Drain the final batch
if ($pendingPromises !== null) {
$results = $translator->resolveAsyncBatch($pendingPromises);
foreach ($pendingBatch as $index => $item) {
$db->save($item["id"], $results[$index]);
}
}This eliminates the idle gap between batches — vLLM sees a continuous stream of requests instead of burst → pause → burst. A worked example with fake DB saves is in examples/pipelined-batch.php.
$detections = $translator->detectBatch([
"Hello world",
"Tere maailm",
"Bonjour le monde",
]);
# Returns detection results for each text concurrently$translator = new LibreTranslate(
host: "https://your-server.com",
port: 9453,
source: "en",
target: "et",
guzzleOptions: [
"timeout" => 300,
"connect_timeout" => 5,
"verify" => true,
],
format: "html", # optional default format (null = auto-detect)
);
### Overload-aware defaults
For LTEngine behind the Smartech proxy, the server may respond with `429 Too Many Requests` and a `Retry-After` header when the GPU is saturated. The client now retries those transient failures automatically.
```php
$translator = new AsyncLibreTranslate("https://your-server.com", 9453);
$translator->setMaxConcurrentRequests(4);
$translator->setRetryPolicy(2, 2); # 2 retries, 2s fallback when Retry-After is missingOn a single RTX 3090, start with 4 concurrent requests and only raise it if the server remains stable.
## Benchmark
CLI tool for measuring translation throughput in sync and async (parallel) modes with response validation.
Validates every response: checks `translatedText` field is present, non-empty, and actually differs from the source text (catches echo-back bugs).
On Linux/macOS with the `pcntl` extension, Ctrl+C triggers graceful shutdown with partial results.
### Usage (benchmark.php)
```bash
# Sync mode (sequential requests)
php benchmark/benchmark.php http://localhost:5000
# Async mode with 24 concurrent workers
php benchmark/benchmark.php http://localhost:5000 --mode=async --repeat=5 --workers=24
# With authentication
php benchmark/benchmark.php https://your-server --port=9453 --auth=Basic:<base64-credentials>
# Custom test cases from JSON file
php benchmark/benchmark.php http://localhost --test-cases=cases.json
# Export results to JSON
php benchmark/benchmark.php http://localhost --mode=async --workers=4 --export=results.json
| Option | Default | Description |
|---|---|---|
--port=PORT |
from URL | API port |
--mode=MODE |
sync |
sync or async |
--repeat=N |
10 |
Times to repeat all test cases |
--workers=N |
4 |
Concurrency level (async mode) |
--source=LANG |
auto |
Source language |
--target=LANG |
et |
Target language |
--timeout=SECONDS |
120 |
Request timeout |
--auth=TYPE:CRED |
— | Authorization header |
--api-key=KEY |
— | API key |
--test-cases=FILE |
— | JSON file with test cases |
--export=FILE |
— | Export results to JSON |
-v, --verbose |
off | Show per-request details |
[
{ "text": "Hello world", "source": "en", "target": "et" },
{ "text": "Goodbye", "source": "en", "target": "ru" }
]src/
├── LibreTranslate.php # Base sync class (Guzzle HTTP)
├── AsyncLibreTranslate.php # Async extension (Guzzle Promises)
└── Benchmark/
├── BenchmarkCli.php # CLI orchestrator (args, signals, exit codes)
├── BenchmarkOutput.php # Header, summary, failures, export
├── BenchmarkResult.php # Single request result data class
├── BenchmarkRunner.php # Sync & async execution with validation
├── BenchmarkStats.php # Aggregate statistics
├── ProgressBar.php # CLI progress bar
├── TestCaseLoader.php # Built-in & JSON test case loading
└── TranslationValidator.php # Response validation (4-step check)LibreTranslate— synchronous client compatible with both LibreTranslate and LTEngine APIs; format auto-detected per-call via regex, overridable via constructor,setFormat(), or per-call parameterAsyncLibreTranslate— extends base class withtranslateAsync(),translateBatch(),translateMultiTarget(),detectBatch(),startAsyncBatch(),resolveAsyncBatch(), andpump()methods; installs aCurlMultiHandlerwithselectTimeout=0.0by default sopump()is non-blocking out of the box;setSelectTimeout()restores the Guzzle default (1.0 s) when needed
This library is designed to work with LTEngine, which provides a LibreTranslate-compatible API powered by OpenAI-compatible models via vLLM.
Supported endpoints:
POST /translate— translate text (withautosource language detection)GET /languages— list available languagesPOST /detect— detect language of given textGET /settings— retrieve server settings and model information
GPL-3.0-or-later
Original library by Jeffrey Shilt (AGPL-3.0).