webfactory/polyglot-bundle
Polyglot Bundle is a Symfony package for multilingual applications, streamlining internationalization with translation and locale-aware content support. It reduces boilerplate around managing languages so teams can ship consistent multi-language experiences faster.
Minimal setup
composer require webfactory/polyglot-bundle and register Webfactory\Bundle\PolyglotBundle\WebfactoryPolyglotBundle in your kernel/bundle list (this is a Symfony/Doctrine bundle — in a Laravel app you'd need the Doctrine ORM bridge plus manual service wiring; it is not Eloquent-native).implements TranslatableInterface and give it a companion Translation entity implementing TranslationInterface.#[TranslatedProperty], and the field that stores the locale with #[Locale].TranslationProxyFactory and inject the proxy.$polyglot->setLocale('de_DE');
$product = $em->find(Product::class, 42);
$product->getName(); // resolves through the translation proxy
Where to look first
TranslatedProperty, Locale) — they define the entire mapping contract.TranslationProxyFactory / the generated proxy — this is where lazy translation loading happens, so it's the first place to look when a value comes back "wrong".Polyglot service (locale handling) — the bundle only ever returns translations for the locale that is currently set.First real use case
A catalogue entity with name, slug, description that must exist per-locale, rendered in whichever locale the request negotiated. Start with a single translatable property (name) on one entity, one locale switch, and verify the round-trip through the proxy before expanding.
Attribute mapping on the translation entity
class ProductTranslation implements TranslationInterface
{
#[Locale]
private string $locale;
#[TranslatedProperty]
private string $name;
#[TranslatedProperty]
private ?string $description = null;
}
Class-level placement is also supported, and as of 4.5.4 class-level TranslatedProperty and Locale attributes are now also read from parent classes. That unlocks the common pattern of a shared abstract translation base:
abstract class AbstractTranslation implements TranslationInterface
{
#[Locale]
protected string $locale;
}
class ProductTranslation extends AbstractTranslation
{
#[TranslatedProperty]
private string $name;
}
Locale-scoped service usage
Set the locale once per request (middleware/event listener), not per query. The proxy resolves on first property access, so the locale at hydration time is what matters.
Persisting translations
Treat the translation collection like any other Doctrine collection: build/attach the translation for the target locale, flush, and let the proxy fall back to the canonical entity when no translation exists for the active locale. For admin UIs, iterate locales explicitly and edit each translation rather than relying on the active-locale proxy.
Integration tips
Metadata/cache after upgrading
The 4.5.4 inheritance fix is a metadata-parsing change. After upgrading, clear Doctrine metadata caches (bin/console cache:clear, plus any second-level/apcu/redis metadata cache). On 4.5.3 and earlier, class-level attributes declared on a parent were silently ignored — that meant a #[Locale] on an abstract base produced a translation entity the bundle couldn't map correctly. If you "worked around" that by re-declaring the attributes on every child class, you can now remove the duplication; leaving both in place is harmless but confusing.
Missing class-level attributes fail quietly
Because attribute discovery is reflection-driven, a missing #[Locale] doesn't throw a helpful error — you get a proxy that never resolves or a locale of null. When a translation "doesn't apply", dump the resolved metadata before debugging anything else.
Locale is global mutable state
The locale is held by the Polyglot service. Long-running workers (queue consumers, Swoole/RoadRunner, messenger handlers) must reset it per job or you will leak the previous job's locale into the next one. This is the single most common production bug with this bundle.
Property-level vs class-level attributes
4.5.4 only changed class-level attribute inheritance. Property-level #[TranslatedProperty] on inherited properties already worked via reflection, but be deliberate: put #[Locale] once on the base class and keep #[TranslatedProperty] on concrete properties, so a future refactor doesn't accidentally mark a shared field as translatable.
Proxies and var_dump/serialization
The translatable object you get back is a generated proxy subclass. Serializing it (cache, API response, queue payload) can drag in the Doctrine unit of work or drop the proxy. Re-hydrate entities on the other side instead of serializing them.
Debugging tips
Extension points
Custom locale resolution, translation entity naming, and proxy generation are all replaceable services — prefer decorating Polyglot (to add negotiation/fallback logic) over subclassing entities, and keep the locale decision in one place so workers, HTTP requests, and CLI commands all behave identically.
How can I help you explore Laravel packages today?