php-translation/common
Shared contracts and utilities for the PHP Translation ecosystem. Provides common interfaces, models, and helpers used across translation bundles to keep integrations consistent and reduce duplication, making it easier to build and maintain translation features in PHP apps.
Installation:
composer require php-translation/common
Ensure your project uses PHP 8.2+ and Symfony 6.4+ (or 7.x).
Basic Usage:
Load translations from a standard .json or .php file:
use PhpTranslation\Common\Loader\JsonFileLoader;
use PhpTranslation\Common\MessageCatalogue;
$loader = new JsonFileLoader();
$catalogue = $loader->load('path/to/translations.json', 'en');
$message = $catalogue->get('key.name');
First Use Case:
Replace Laravel’s default trans() helper for a specific module (e.g., API responses):
// In AppServiceProvider
$this->app->bind(\PhpTranslation\Common\TranslatorInterface::class, function () {
$loader = new JsonFileLoader();
$catalogue = $loader->load(resource_path('lang/en.json'), 'en');
return new \PhpTranslation\Common\Translator($catalogue);
});
// Usage in routes/controllers
$translator = app(\PhpTranslation\Common\TranslatorInterface::class);
return $translator->trans('validation.required');
MessageCatalogue (core interface for translations).Loader interfaces (JsonFileLoader, PhpFileLoader).Translator (for translating messages with placeholders).tests directory for integration patterns.SymfonyBridge for Laravel-Symfony interop.$loader = new JsonFileLoader();
$catalogue = $loader->load('lang/en.json', 'en');
LoaderInterface:
class DatabaseLoader implements LoaderInterface {
public function load(string $path, string $locale): MessageCatalogue {
$data = DB::table('translations')->where('locale', $locale)->get();
return new MessageCatalogue($data->toArray());
}
}
Use middleware to set the locale:
// app/Http/Middleware/SetLocale.php
public function handle(Request $request, Closure $next) {
$locale = $request->header('Accept-Language') ?? config('app.fallback_locale');
app()->setLocale($locale);
return $next($request);
}
Bind a locale-aware translator:
$this->app->singleton(TranslatorInterface::class, function () {
$loader = new JsonFileLoader();
$locale = app()->getLocale();
return new Translator($loader->load("lang/{$locale}.json", $locale));
});
Leverage Symfony’s Intl integration:
use PhpTranslation\Common\Pluralization\Pluralizer;
use Symfony\Component\Intl\Intl;
$pluralizer = new Pluralizer(Intl::getLocaleBundle());
$pluralized = $pluralizer->pluralize('message.key', 5, ['%count%' => 5]);
Replace Laravel’s default validator messages:
$validator = Validator::make($data, [
'email' => 'required|email',
], [], [], [
'email.required' => trans('validation.custom.email_required'),
]);
Use the package’s Translator to load custom messages:
$catalogue = $loader->load('lang/validation.json', 'en');
$translator = new Translator($catalogue);
$validator->setCustomMessages($translator->getAll());
Laravel Service Container: Bind interfaces to concrete implementations:
$this->app->bind(LoaderInterface::class, JsonFileLoader::class);
$this->app->bind(TranslatorInterface::class, function ($app) {
$loader = $app->make(LoaderInterface::class);
return new Translator($loader->load('lang/' . app()->getLocale() . '.json', app()->getLocale()));
});
Caching:
Cache MessageCatalogue instances:
$catalogue = Cache::remember("translations_{$locale}", now()->addHours(1), function () use ($loader, $locale) {
return $loader->load("lang/{$locale}.json", $locale);
});
Fallback Locales:
Implement a FallbackCatalogue decorator:
class FallbackCatalogue implements MessageCatalogueInterface {
public function __construct(
private MessageCatalogueInterface $primary,
private MessageCatalogueInterface $fallback
) {}
public function get(string $id, array $parameters = []): string {
return $this->primary->has($id)
? $this->primary->get($id, $parameters)
: $this->fallback->get($id, $parameters);
}
}
Testing:
Mock MessageCatalogue in tests:
$catalogue = $this->createMock(MessageCatalogueInterface::class);
$catalogue->method('get')->willReturn('Mocked translation');
$translator = new Translator($catalogue);
$this->assertEquals('Mocked translation', $translator->trans('test.key'));
Locale Mismatches:
en_US as locale but loading en.json files.en for en_US):
$locale = strtok($locale, '_'); // 'en_US' -> 'en'
Circular References in Translations:
key1 uses key2, which uses key1).DepthLimitException handler or implement a cycle detector in custom loaders.Symfony Version Conflicts:
^6.4 or upgrade to Symfony 7:
composer require symfony/intl:^7.0
Placeholder Syntax:
{0} placeholders vs. Symfony’s %var%.Translator with Symfony-style placeholders:
$translator->trans('message.key', ['%var%' => 'value']);
File Permissions:
JsonFileLoader failing silently on unreadable files.try {
$catalogue = $loader->load('unreadable.json', 'en');
} catch (FileNotFoundException $e) {
Log::error("Translation file missing: {$e->getMessage()}");
$catalogue = $fallbackLoader->load('en.json', 'en');
}
Enable Debug Mode:
$translator = new Translator($catalogue, Translator::DEBUG_MODE);
This logs missing keys and untranslated messages.
Inspect Catalogue Contents:
dump($catalogue->getAll()); // View all loaded translations
Check Loader Paths: Use absolute paths for testing:
$loader->load(__DIR__ . '/../lang/en.json', 'en');
Custom Loaders:
Implement LoaderInterface for databases, APIs, or S3:
class S3Loader implements LoaderInterface {
public function load(string $path, string $locale): MessageCatalogue {
$data = S3::getObject($path)->get('Body')->toArray();
return new MessageCatalogue($data);
}
}
MessageCatalogue Decorators: Add logic before/after translation:
class LoggingCatalogue implements MessageCatalogueInterface {
public function __construct(private MessageCatalogueInterface $catalogue) {}
public function get(string $id, array $parameters = []): string {
Log::debug("Translating: {$id}", $parameters);
return $this->catalogue->get($id, $parameters);
}
}
Pluralization Rules: Override default rules for
How can I help you explore Laravel packages today?