survos / translator-bundle
Engine-agnostic translation client bundle (Symfony 7.3 / PHP 8.4) with LibreTranslate adapter and optional caching.
Fund package maintenance!
kbond
Installs: 16
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
Requires
- php: ^8.4
- psr/cache: ^3.0
- psr/container: ^2.0
- symfony/cache: ^7.3
- symfony/config: ^7.3
- symfony/console: ^7.3
- symfony/dependency-injection: ^7.3
- symfony/http-client: ^7.3
- symfony/http-kernel: ^7.3
- symfony/string: ^7.3
Requires (Dev)
- phpunit/phpunit: ^12
This package is auto-updated.
Last update: 2025-09-10 15:07:53 UTC
README
A Symfony 7.3 / PHP 8.4 bundle that unifies multiple translation engines (DeepL, LibreTranslate, …), adds smart caching, and optional async processing via Messenger. Designed to replace the legacy libre-bundle
in the Survos translation server.
Target repo to integrate with next: https://github.com/survos-sites/translation-server
Features
- ✅ Drop‑in service:
Survos\TranslatorBundle\Service\Translator
- 🔌 Pluggable engines: DeepL, LibreTranslate (more welcome)
- 🧠 Cache‑first: Symfony Cache (PSR‑6/16) with per‑engine TTL & busting
- 🚀 Async mode: Messenger message + worker for heavy workloads
- 📝 Rich metadata: hash, engine, source/target, confidence, token counts
- 🧪 Handy CLI for quick checks & warmups
Installation
composer require survos/translator-bundle
If you use Symfony Flex, the bundle is auto‑enabled. Otherwise, add to config/bundles.php
:
return [ // ... Survos\TranslatorBundle\SurvosTranslatorBundle::class => ['all' => true], ];
Environment & API Keys
Set the following in your .env.local
(or server secrets). Only configure the engines you’ll use.
### Core ### TRANSLATOR_DEFAULT_ENGINE=libre # libre | deepl TRANSLATOR_CACHE_TTL=86400 # seconds (1 day default) TRANSLATOR_TIMEOUT=10 # HTTP seconds ### DeepL ### DEEPL_API_KEY=\!\!put-your-key-here\!\! # Optional: free vs pro endpoint auto‑detected by key suffix (-free). Override if needed: DEEPL_BASE_URI=https://api-free.deepl.com/v2 ### LibreTranslate ### LIBRETRANSLATE_BASE_URI=https://translate.argosopentech.com # If your instance requires a key: LIBRETRANSLATE_API_KEY=
Pro tip: keep engine‑specific keys/names distinct per environment to avoid accidental cross‑use.
Bundle Configuration
Create config/packages/survos_translator.yaml
:
survos_translator: default_engine: '%env(string:TRANSLATOR_DEFAULT_ENGINE)%' timeout: '%env(int:TRANSLATOR_TIMEOUT)%' cache_ttl: '%env(int:TRANSLATOR_CACHE_TTL)%' engines: deepl: api_key: '%env(DEEPL_API_KEY)%' base_uri: '%env(default:~:DEEPL_BASE_URI)%' # null => autodetect libre: base_uri: '%env(LIBRETRANSLATE_BASE_URI)%' api_key: '%env(default::LIBRETRANSLATE_API_KEY)%' # may be empty
Optional: Dedicated Cache Pool
framework: cache: pools: survos_translator.cache: adapter: cache.app default_lifetime: 86400
The bundle will auto‑wire a pool named survos_translator.cache
if present, otherwise falls back to cache.app
.
Quick Start (Sync)
<?php namespace App\Controller; use Survos\TranslatorBundle\Service\Translator; // the facade use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; final class DemoController extends AbstractController { public function translate(Translator $translator): Response { $result = $translator->translate( text: 'Hello world', target: 'es', // ISO 639-1 or BCP-47; engine normalizes source: 'en', // optional; auto‑detect if omitted domain: 'ui', // optional context tag for caching options: [ // engine‑specific extras 'formality' => 'prefer_less', // DeepL example ] ); // $result is a DTO with: text, source, target, engine, detectedSource, meta, cached return new Response($result->text); // "Hola mundo" } }
Minimal Service Call (no controller)
$translated = $translator->translate('Save', 'es');
Async Translation (Messenger)
Enable a transport (choose one) in config/packages/messenger.yaml
:
framework: messenger: transports: translator: '%env(MESSENGER_TRANSPORT_DSN)%' # e.g. doctrine://default | redis://localhost | amqp://... routing: Survos\TranslatorBundle\Message\TranslateText: translator
Dispatch work:
use Survos\TranslatorBundle\Message\TranslateText; use Symfony\Component\Messenger\MessageBusInterface; $bus->dispatch(new TranslateText('Hello world', 'es', source: 'en', domain: 'ui'));
Run a worker:
php bin/console messenger:consume translator -vv
The worker uses the same caching rules; repeated requests are cheap.
CLI Utilities (Symfony 7.3 style)
The bundle ships a small demo command to smoke‑test config and warm cache.
php bin/console translator:demo "Hello" --to=es --from=en --engine=deepl
Sample implementation pattern (your app command):
<?php namespace App\Command; use Survos\TranslatorBundle\Service\Translator; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\Option; #[AsCommand('translator:demo')] final class TranslatorDemoCommand { public function __construct(private Translator $translator) {} public function __invoke( SymfonyStyle $io, #[Argument('Phrase to translate')] string $text, #[Option('to')] ?string $to = null, #[Option('from')] ?string $from = null, #[Option('engine')] ?string $engine = null, ): int { $res = $this->translator->translate($text, $to ?? 'es', $from, options: ['engine' => $engine]); $io->success(sprintf('[%s] %s → %s: %s', $res->engine, $res->source ?? 'auto', $res->target, $res->text)); return Command::SUCCESS; } }
Notes for this user/project: parameters follow your preferred Symfony 7.3 attribute style (invokable,
SymfonyStyle
, attributes in__invoke
).
Engine Behavior
-
DeepL
- Auto‑selects Free/Pro endpoint by key suffix (
-free
= Free); can override viaDEEPL_BASE_URI
. - Supports options:
formality
,glossary_id
,preserve_formatting
, etc.
- Auto‑selects Free/Pro endpoint by key suffix (
-
LibreTranslate
- Works with any compatible instance; set
LIBRETRANSLATE_BASE_URI
. - Some deployments require
LIBRETRANSLATE_API_KEY
; others don’t.
- Works with any compatible instance; set
Both engines normalize language codes and report back detectedSource
when source is omitted.
Caching Strategy
- Key = hash(text, source, target, engine, domain, options subset)
- Default TTL via
TRANSLATOR_CACHE_TTL
or cache pool lifetime - Bust per domain/engine using provided cache‑clearer:
php bin/console translator:cache:clear --engine=libre --domain=ui
Error Handling
- Network/HTTP errors raise
TranslationTransportException
- Invalid configuration raises
TranslationConfigException
- Engines return
TranslationResult
withmeta["cached"] = true|false
Catch & fallback example:
try { $res = $translator->translate('Hello', 'fr', options: ['engine' => 'deepl']); } catch (\Throwable $e) { // Fallback to Libre $res = $translator->translate('Hello', 'fr', options: ['engine' => 'libre']); }
Replacing the Legacy libre-bundle
- Remove old services/config tied to
libre-bundle
. - Install this bundle and add envs:
LIBRETRANSLATE_BASE_URI
, optionalLIBRETRANSLATE_API_KEY
. - Search/Replace old client/service with
Survos\TranslatorBundle\Service\Translator
. - Switch endpoints: old
/translate
controllers can now delegate to the new service. - Enable async in the translation server by routing
TranslateText
via Messenger. - Keep your existing cache: point the pool name to
survos_translator.cache
.
Minimal controller in translation‑server style:
#[Route('/api/translate', name: 'api_translate', methods: ['POST'])] public function api(Request $req, Translator $translator): JsonResponse { $text = (string) $req->request->get('text', ''); $to = (string) $req->request->get('to', 'es'); $from = $req->request->get('from'); $engine = $req->request->get('engine'); $res = $translator->translate($text, $to, $from, options: ['engine' => $engine]); return $this->json([ 'text' => $res->text, 'source' => $res->source ?? $res->detectedSource, 'target' => $res->target, 'engine' => $res->engine, 'cached' => (bool)($res->meta['cached'] ?? false), ]); }
Testing Locally
# 1) Provide env vars (see above) cp .env .env.local && $EDITOR .env.local # 2) Quick smoke‑test php bin/console translator:demo "Hello world" --to=es # 3) Try the controller symfony server:start -d curl -X POST https://127.0.0.1:8000/api/translate -F text='Hello' -F to=fr
Extending with New Engines
- Implement
EngineInterface
(e.g.,AcmeEngine
). - Tag the service with
survos.translator.engine
and give it aname
. - Now you can call with
options: ['engine' => 'acme']
.
Skeleton:
final class AcmeEngine implements EngineInterface { public function name(): string { return 'acme'; } public function translate(TranslationInput $in): TranslationResult { /* ... */ } }
Roadmap
- Google/Bing providers
- Glossaries & per‑project domains
- Token accounting & quotas per engine
- Batch API for paragraphs (keeps caching per unit)
License
MIT © Survos