symfony/translation-contracts
Symfony Translation Contracts provides lightweight interfaces and abstractions for translation in PHP, extracted from Symfony components. Use it to build interoperable, battle‑tested translation integrations while staying framework-agnostic and compatible with Symfony implementations.
## Getting Started
### Minimal Setup
1. **Install the package** (already included in Laravel via `symfony/translation`):
```bash
composer require symfony/translation-contracts
(Note: Laravel 9+ already bundles this via symfony/translation, so no explicit install is needed unless extending functionality.)
Locate core interfaces in vendor/symfony/translation-contracts/src/Translation/:
TranslatorInterface: Core translation contract (e.g., trans(), getLocale()).TranslatableInterface: For deferred translation (e.g., TranslatableMessage).MessageCatalogueInterface: For locale/message domain management.First use case: Extend Laravel’s built-in translator by implementing a custom adapter. Example:
use Symfony\Contracts\Translation\TranslatorInterface;
// Bind a custom translator to Laravel's container
$app->bind(TranslatorInterface::class, function ($app) {
return new CustomTranslator($app['translator']); // Wrap Laravel's translator
});
trans() helper already implements TranslatorInterface. Use it directly:
$translator = app(TranslatorInterface::class);
$message = $translator->trans('validation.required', ['attribute' => 'email']);
domain parameter (e.g., validation, auth) to isolate message catalogs:
$translator->trans('welcome', [], 'auth');
TranslatableInterfaceuse Symfony\Contracts\Translation\TranslatableMessage;
$message = new TranslatableMessage('validation.required', ['attribute' => 'email']);
// Later, translate when locale is known:
$translated = $translator->trans($message);
TranslatableMessage in view data or Livewire properties.use Symfony\Contracts\Translation\TranslatorInterface;
class AwsTranslator implements TranslatorInterface {
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string {
return aws_translate($id, $parameters, $locale);
}
// ... other required methods
}
$app->bind(TranslatorInterface::class, function () {
return new AwsTranslator(config('services.aws'));
});
App::setLocale() or middleware:
$translator->setLocale(request()->header('Accept-Language') ?? 'en');
TranslatorTrait for built-in fallback support:
use Symfony\Component\Translation\TranslatorTrait;
class FallbackTranslator implements TranslatorInterface {
use TranslatorTrait;
protected function doTrans($id, array $parameters = [], $domain = null, $locale = null) {
// Custom logic (e.g., check DB, then fallback to Laravel's translator)
}
}
TranslatorInterface to isolate logic:
$mockTranslator = $this->createMock(TranslatorInterface::class);
$mockTranslator->method('trans')
->with('welcome', ['name' => 'John'], 'auth', 'en')
->willReturn('Welcome, John!');
$this->app->instance(TranslatorInterface::class, $mockTranslator);
No Concrete Implementation:
trans() helper), orsymfony/translation, google/cloud-translate).TranslatorInterface binding points to a real translator.Parameter Syntax Mismatches:
trans() uses :placeholder syntax, but symfony/translation supports ICU MessageFormat (e.g., {count, plural, one{...} other{...}}).trans() for Laravel-native strings; ICU syntax for Symfony-compatible messages.Missing Pluralization Helpers:
transChoice() (e.g., for "1 item" vs. "2 items"). Laravel provides this via trans_choice().function transChoice($id, $number, array $parameters = [], $domain = null) {
return app(TranslatorInterface::class)->trans(
$id . ($number == 1 ? '' : '_plural'),
$parameters,
$domain
);
}
Locale Not Persisted:
setLocale()) is not the same as Laravel’s App::setLocale().public function handle(Request $request, Closure $next) {
$locale = $request->header('X-Locale') ?? 'en';
app()->setLocale($locale);
app(TranslatorInterface::class)->setLocale($locale);
return $next($request);
}
Domain Isolation Issues:
domain parameter, translations may pull from the wrong catalog.domain in trans() calls to verify it’s being passed correctly.Extend TranslatorTrait for Boilerplate:
use Symfony\Component\Translation\TranslatorTrait;
class CustomTranslator implements TranslatorInterface {
use TranslatorTrait;
protected function doTrans($id, array $parameters = [], $domain = null, $locale = null) {
// Your custom logic here
}
}
Use TranslatableMessage for API Responses:
TranslatableMessage objects in JSON:API or GraphQL responses to defer translation until the client’s locale is known.Cache Translator Instances:
$app->singleton(TranslatorInterface::class, function () {
return new AwsTranslator();
});
Validate Locale Formats:
en_US vs. en-US). Use Locale::getPrimaryLanguage() to normalize:
use Symfony\Component\Translation\Locale;
$normalized = Locale::getPrimaryLanguage($locale);
Debugging Silent Failures:
trans() wrapper to log unresolved messages:
$translator = app(TranslatorInterface::class);
$translator->trans = function ($id, $params = [], $domain = null, $locale = null) use ($translator) {
$result = $translator->trans($id, $params, $domain, $locale);
if (strpos($result, '{{') !== false) { // Untranslated placeholder
Log::warning("Untranslated ID: {$id} (Locale: {$locale}, Domain: {$domain})");
}
return $result;
};
Laravel-Specific Quirks:
TranslatableMessage in view composers to pass deferred translations to Blade:
View::composer('*', function ($view) {
$view->with('welcomeMessage', new TranslatableMessage('welcome'));
});
public $translatableMessage;
public function mount() {
$this->translatableMessage = new TranslatableMessage('validation.required');
}
public function render() {
return view('livewire.component', [
'message' => app(TranslatorInterface::class)->trans($this->translatableMessage)
]);
}
---
How can I help you explore Laravel packages today?