Quick Start Guide
Get started with derafu/translation in a few minutes.
Installation
composer require derafu/translation
That’s it — there are no optional dependencies to add separately.
symfony/translation, symfony/translation-contracts, symfony/yaml and
the intl PHP extension all come as hard requirements, so every built-in
file format (YAML, JSON, PHP, XLIFF, PO, MO, CSV, INI) and ICU formatting
work out of the box.
Basic Setup
1. Make an Exception Translatable
Extend one of the built-in translatable exceptions:
use Derafu\Translation\Exception\Core\TranslatableException;
class ValidationException extends TranslatableException
{
// No additional code needed.
}
If you can’t change the parent class (you already extend something else), use the trait instead:
use Derafu\Translation\Contract\TranslatableInterface;
use Derafu\Translation\Trait\TranslatableExceptionTrait;
use DomainException;
class ValidationException extends DomainException implements TranslatableInterface
{
use TranslatableExceptionTrait;
}
See Exceptions for the full hierarchy.
2. Throw It
// 1. A plain string: used as-is, and as the translation id.
throw new ValidationException('Email is required.');
// 2. An array: first element is the message/id, the rest are named ICU
// parameters.
throw new ValidationException([
'The value {value} must be between {min} and {max}.',
'value' => 42,
'min' => 1,
'max' => 10,
]);
getMessage() already returns the fully formatted English text at this
point — no translator needed yet:
try {
// ...
} catch (ValidationException $e) {
echo $e->getMessage();
// "The value 42 must be between 1 and 10."
}
3. Create Translation Files
Files are flat inside a directory and follow the naming convention
{domain}(+intl-icu)?.{locale}.{format}. TranslatableExceptionTrait
defaults to the errors domain, so add +intl-icu to activate ICU
formatting for it:
// translations/errors+intl-icu.en.php
return [
'The value {value} must be between {min} and {max}.' =>
'The value {value} must be between {min} and {max}.',
];
// translations/errors+intl-icu.es.php
return [
'The value {value} must be between {min} and {max}.' =>
'El valor {value} debe estar entre {min} y {max}.',
];
YAML and JSON work exactly the same way:
# translations/errors+intl-icu.es.yaml
'The value {value} must be between {min} and {max}.': 'El valor {value} debe estar entre {min} y {max}.'
// translations/errors+intl-icu.es.json
{
"The value {value} must be between {min} and {max}.": "El valor {value} debe estar entre {min} y {max}."
}
4. Build a Translator and Register the Files
use Derafu\Translation\TranslationResourceRegistrar;
use Derafu\Translation\TranslatorFactory;
// Builds a Symfony Translator with every supported loader already wired.
$translator = TranslatorFactory::create(
defaultLocale: 'es',
fallbackLocales: ['es', 'en'],
);
// Discovers every translation file in the directory and registers it.
$registrar = new TranslationResourceRegistrar($translator);
$registrar->registerDirectory(__DIR__ . '/translations');
5. Translate
try {
// ...
} catch (ValidationException $e) {
// Untranslated (English, formatted eagerly at construction time).
echo $e->getMessage();
// Translated to the translator's current locale.
echo $e->trans($translator);
// Translated to a specific locale.
echo $e->trans($translator, 'es');
}
Dependency injection
For a standalone Derafu application, import the complete service configuration:
# config/services.yaml
imports:
- { resource: '../vendor/derafu/translation/resources/config/translation-services.yaml' }
parameters:
derafu.translation.default_locale: es
derafu.translation.fallback_locales: [en]
The package registers one shared Symfony\Contracts\Translation\TranslatorInterface
service directly with Symfony’s component translator and TranslatorFactory.
Defaults are locale en and an empty fallback list. The factory installs the
supported loaders and consumes all derafu_translation.resource_provider tags
while creating the translator, so the caller does not fetch a separate registrar.
Packages contribute their own tagged providers. An application can add one too:
services:
Derafu\Translation\Contract\TranslationResourceProviderInterface $application:
class: Derafu\Translation\SimpleTranslationResourceProvider
arguments:
$directories: ['/path/to/application/translations']
tags: ['derafu_translation.resource_provider']
Foundation imports the complete configuration and maps its existing app.locale
setting to derafu.translation.default_locale, with [en] as its fallback list.
Sites keep using APP_LOCALE and app.locale.default.
For native Symfony applications, use
derafu/symfony-translation-bundle. The bundle
imports translation-resources-services.yaml, which registers only Translation’s
catalogue provider. That resources-only configuration declares no translator,
alias or locale parameters: the bundle adds resources to FrameworkBundle’s
existing translator and preserves its configuration. Do not import the standalone
configuration into that integration.
Common Use Cases
Form-Style Validation
class UserValidator
{
public function validateEmail(string $email): void
{
if (empty($email)) {
throw new ValidationException('The field {field} is required.', ['field' => 'email']);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new ValidationException([
'The value {value} is not a valid email.',
'value' => $email,
]);
}
}
}
API Error Responses
use Derafu\Translation\Contract\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
class ApiErrorHandler
{
public function __construct(
private readonly TranslatorInterface $translator,
) {
}
public function handle(Throwable $e): array
{
$message = $e instanceof TranslatableInterface
? $e->trans($this->translator)
: $e->getMessage();
return ['error' => ['message' => $message]];
}
}
Complex Messages (Pluralization, Gender)
throw new ValidationException(
'{count, plural, =0{No files uploaded} one{1 file uploaded} other{# files uploaded}}',
['count' => $fileCount],
);
throw new ValidationException(
'{gender, select, female{She} male{He} other{They}} uploaded {count} files.',
['gender' => $user->getGender(), 'count' => $fileCount],
);
Next Steps
- Read about ICU Message Format for pluralization, gender selection and more.
- Learn about Resource Providers for multi-directory and multi-package setups.
- Check the API Reference for every class and interface.
- See Advanced Usage for composition and DI wiring.
Common Pitfalls
-
Always include the
othercase in plural/select patterns:// Wrong. '{gender, select, female{She} male{He}}' // Correct. '{gender, select, female{She} male{He} other{They}}' -
Provide every parameter the message uses:
// Missing 'field' — the {field} placeholder is left unexpanded. throw new ValidationException('The field {field} is required.'); // getMessage(): "The field {field} is required." // Correct. throw new ValidationException('The field {field} is required.', ['field' => 'email']); // getMessage(): "The field email is required." -
Remember the
+intl-icusuffix on the domain if your messages use ICU syntax ({name}, plural, select). Without it,symfony/translationuses plain substitution instead: it replaces the name of each parameter wherever it appears, so{name}becomes{John}. See ICU Formatting.// errors.es.php (no `+intl-icu`): the entry exists, but ICU is not applied. 'Hello {name}' => 'Hola {name}' // $e->trans($translator): "Hola {John}" // errors+intl-icu.es.php: ICU is applied. // $e->trans($translator): "Hola John"A message with no entry at all is not affected: it is formatted as
getMessage()is (see ICU Formatting).
Tips & Tricks
-
Add static factory methods to your exceptions for common cases:
class ValidationException extends TranslatableException { public static function required(string $field): self { return new self(['The field {field} is required.', 'field' => $field]); } } -
Use type hints and PHPDoc for better IDE support:
/** * @throws ValidationException When the email is invalid. */ public function validateEmail(string $email): void { // ... }