skippy565/object-translation-bundle
A Symfony bundle for locale-aware translation of object properties, helping you store and retrieve localized field values without duplicating entities. Useful for multilingual content and simple i18n workflows.
This is a small, low-traffic Symfony bundle (last release June 2018, 2 stars, zero dependents), so treat the README as the primary — and possibly only — source of truth. Budget time to read the code before wiring it in; there is no community knowledge base to fall back on.
Minimal setup:
composer require skippy565/object-translation-bundle
Then register it. On Symfony 3.x / 4.0–4.3 (the era this targets) that means AppKernel::registerBundles(); if you're on a Flex app, add it to config/bundles.php. Check composer.json first — the symfony/* and php constraints will tell you whether this installs at all on your stack. If you're on Symfony 5+, expect to need --ignore-platform-reqs at best and real code changes at worst.
Where to look first, in order:
DependencyInjection/Configuration.php — the full config tree with defaults. This is the fastest way to learn what's configurable.Resources/config/services.* — service IDs, aliases, and which class actually does the translation work.DependencyInjection/*Extension.php — how config maps onto services (and which services are conditional).Entity/, Model/, or Annotation/ directory — this is where the translation storage and the @Translatable-style markers live.README.md last, so you can tell which parts are stale.First use case to prove out: pick one entity with a single translated string field (a product name, a category label), configure it for two locales, and read it back under each locale in a controller. That exercises the whole path — metadata discovery, translation lookup, locale resolution — without you committing to a schema-wide refactor. Once that round-trips, expand field by field.
If the bundle turns out to be a thin wrapper over an existing approach (Doctrine event listeners plus a translation table), the codebase is probably small enough to read end-to-end in an afternoon. Do that before designing around it.
Service-layer translation. The usual shape for a bundle like this is a manager/translator service you inject and call with an object plus a field (or access it via a magic getter / proxy object). Prefer injecting the named service over reaching into the container, and prefer a narrow wrapper of your own around it — that wrapper is your escape hatch when the bundle is abandoned.
class ProductPresenter
{
public function __construct(private ObjectTranslator $translator) {}
public function name(Product $product, string $locale): string
{
return $this->translator->translate($product, 'name', $locale);
}
}
Locale is explicit, not ambient. Pass the locale into your translation calls rather than relying on the request's locale inside the domain layer. You will want to render a page in one locale while emailing a customer in another, and request-scoped locale resolution makes that painful.
Write path. Translation bundles almost always separate read from write: you have a setTranslation($object, $field, $locale, $value) (or an attached translation entity) that must be persisted through Doctrine. Establish one service method that does "set + flush" and one that does "set only," so batch imports don't flush per field.
Fallback chain. Configure a fallback locale (e.g. en → de) and decide early whether a missing translation returns null, an empty string, or the fallback value. Surface that decision in your own wrapper so the rest of the app doesn't have to care.
Form and admin integration. The typical pattern is an embedded collection of translation forms, one sub-form per locale, bound to the translation entity or to a DTO. If the bundle ships form types, use them; if it doesn't, build a TranslationType per locale and map it onto the write path above. Keep the admin editing surface separate from the front-end read path — the read path should never depend on form classes.
Twig. If a Twig extension is registered, use it for read-only display ({{ product|trans_field('name') }}), but keep any logic-dependent formatting in PHP. Check Resources/config/ for the extension service and its filter name — it won't necessarily be trans.
Bulk reads. Any per-object translator call risks an N+1 against the translation table. For listings, prefetch translations for the whole result set in one query (usually a WHERE object_class = ? AND object_id IN (?) AND locale = ?) and hydrate from that map. If the bundle has no batch API, write one against its repository — this is the single most valuable optimization you'll make.
services.yml schema changes, removed DefinitionDecorator, and PHP 8 deprecations (dynamic properties, ${var} in strings). Run the code under your actual PHP version early; static analysis (phpstan at level 5+) will find most of the rot fast.instanceof and === checks against the original entity can then behave unexpectedly — especially inside Doctrine's identity map and with spl_object_id. Keep translated values as strings; don't pass translated objects into EntityManager::persist().object_class + object_id fail for objects that aren't persisted yet (no ID) or have been detached. Guard the write path so translations are attached after the entity is flushed.services.* and either disable it in dev or wire an invalidation call into your save path.%kernel.default_locale%). Dump the processed config (bin/console debug:config <alias>) rather than reading the YAML and assuming.bin/console debug:container <service-id>, debug:event-dispatcher (look for Doctrine lifecycle listeners), and the profiler's Doctrine panel to confirm the translation query count. If translations "aren't saving," check the listener is registered before blaming the bundle.repositories VCS entry is cleaner and keeps your diff reviewable.How can I help you explore Laravel packages today?