Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Entity Translation Bundle Laravel Package

hostnet/entity-translation-bundle

A Symfony bundle for translating Doctrine entity fields per locale. Store and retrieve localized entity content with minimal setup, making multilingual applications easier to build and maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is (and isn't). This bundle renders human-readable labels for enum-like classes — classes whose constants represent a fixed set of values (OrderStatus::PENDING, Gender::MALE). It reads metadata on the class and its constants, then runs the resulting key through Symfony's translator. It is not a Doctrine entity-translation solution: it does not store per-row translated content, so don't reach for it when you need translated database columns.

Version floor moved (1.0.11). The release drops Symfony < 6.4 and PHP < 8.3. If your app is still on PHP 8.1/8.2 or Symfony 6.0–6.3, pin to the previous 1.0.x line — this was published as a patch version but is breaking for older stacks. Check composer why-not hostnet/entity-translation-bundle 1.0.11 before running composer update broadly.

Prerequisites. PHP 8.3+, Symfony 6.4+, and symfony/translation. Note what changed: 1.0.11 removes the doctrine/annotations dependency, which the package declared but never actually used in code. That dependency was doing you an accidental favour — it guaranteed an annotation reader was installed. It no longer is. If your metadata lives in docblocks, you must now require doctrine/annotations yourself and register a reader. On Symfony 7 the framework wires no annotation reader for you, so this is doubly true.

composer require hostnet/entity-translation-bundle
composer require --dev doctrine/annotations   # only if you annotate via docblocks

Register. No Flex recipe at this adoption level (single-digit stars, zero dependents), so add it by hand in config/bundles.php:

Hostnet\Bundle\EntityTranslationBundle\HostnetEntityTranslationBundle::class => ['all' => true],

Where to look first. Read the README, then spend five minutes on two files in vendor/hostnet/entity-translation/src/: the Annotation/ directory (what metadata is actually read, and — critically — which Reader interface it type-hints) and Translator/TranslatorInterface.php. The interface is roughly one screen long and will settle every ambiguity the README leaves open. Confirm what got autowired with bin/console debug:container | grep -i entity_translation.

First use case. A status dropdown. Annotate the class with a domain, annotate each constant with a translation key:

namespace App\Enum;

use Hostnet\Component\EntityTranslation\Annotation as Translation;

/**
 * @Translation\Domain("enums")
 */
final class OrderStatus
{
    /** @Translation\Translation("order_status.pending") */
    public const PENDING = 'pending';

    /** @Translation\Translation("order_status.shipped") */
    public const SHIPPED = 'shipped';
}
# translations/enums.en.yaml
order_status:
    pending: 'Awaiting payment'
    shipped: 'On its way'

Then inject the bundle's translator (do not confuse it with Symfony's TranslatorInterface) and translate the constant:

$label = $this->entityTranslator->translate(OrderStatus::class, OrderStatus::PENDING);

If that signature doesn't line up with what you see in TranslatorInterface, you're on a version where the entity instance is the first argument — check before writing more code.

Implementation Patterns

One domain per bounded context, not per class. @Domain("enums") shared across all your enum-likes keeps translation files navigable (enums.en.yaml, enums.nl.yaml). Per-class domains produce a sprawl of tiny files that translators hate.

Key on the constant name, or key on the value — pick one and enforce it. Keying on the value (order_status.pending) survives constant renaming but breaks if you ever change the stored value. Keying on the name is refactor-hostile. Either way, put the decision in a short comment in the enum class so the next developer doesn't mix styles within a file.

Wrap it behind a one-method adapter. Given the ecosystem size and the version-floor churn in 1.0.11, expose your own LabelProvider (or a Translatable interface on your enums) and let the bundle be an implementation detail. Migrating to native TranslatableInterface on backed enums becomes a one-file change instead of a grep across the codebase.

Symfony Forms. The natural fit is choice_label:

$builder->add('status', ChoiceType::class, [
    'choices' => OrderStatus::cases(),
    'choice_label' => fn ($choice) => $this->labels->for($choice),
]);

For enums that appear in many forms, extract a small EnumType extension so the translation logic lives in one place.

API output. Normalize to ['value' => $status->value, 'label' => $this->labels->for($status)] in a serializer normalizer or a presenter — never leak the raw constant to clients if the UI is expected to display it.

Templates. If the bundle ships a Twig extension, register it and use the filter; if not, add your own 10-line filter over your adapter. Templates should not receive translators via app.request-style globals.

Testing. Assert labels, not keys, in functional tests — that's the behaviour you actually care about. A KernelTestCase with self::getContainer()->get('translator') plus a loop over every constant in the enum is a cheap way to catch missing translations in CI:

foreach ((new \ReflectionClass(OrderStatus::class))->getConstants() as $constant) {
    self::assertNotSame($constant, $this->labels->for($constant), 'Missing label for '.$constant);
}

Fallback strategy. Decide explicitly whether a missing translation should return the key (loud, good in dev) or the raw value (quiet, good in prod). Most teams want the key in dev/test and a sensible fallback in prod.

Bootstrap assertion. Given that 1.0.11 makes the metadata reader your responsibility, add a smoke test that translates one known constant and asserts the output differs from the key. It fails loudly at build time if someone re-installs without doctrine/annotations, instead of failing silently in a dropdown months later.

Gotchas and Tips

  • The annotation reader is now your problem. 1.0.11 removed doctrine/annotations as a dependency because the package never referenced it directly. Nothing in the bundle changed how metadata is read — which means if the reader disappears from vendor/ (fresh installs, composer update on a clean lock, CI containers), the bundle finds zero metadata and translates nothing, silently. After upgrading, run composer why doctrine/annotations; if the answer is nothing, and you annotate via docblocks, add it back explicitly.
  • Docblock annotations on Symfony 7 still need explicit wiring. Combine the above with the fact that Symfony 7 ships no default Reader. If your app is attributes-first, this bundle's docblock parser will see nothing regardless — plan for a custom attribute driver rather than debugging translations that were never read.
  • Cache invalidation bites twice. Docblock metadata is cached (annotation reader cache) and translations are cached. After editing annotations or YAML, run bin/console cache:clear before concluding the bundle is broken.
  • Diagnose with debug:translation. bin/console debug:translation en --domain=enums shows exactly which keys the container knows about. If the key is absent, your domain name or file naming is wrong, not your annotation. If the key is present but the label renders as the key, the reader is the suspect.
  • Domain/file mismatch. The @Domain value must match the file (enums.en.yaml) and the directory must be one Symfony actually scans (translations/, or whatever framework.translator.default_path says).
  • Locale in CLI and queue workers. Translation resolves against the request locale where available; console commands and messenger workers have none. Set the locale explicitly or you'll get the default locale's labels in emails.
  • Naming collision with Symfony's translator. Two TranslatorInterface types in the container is a classic autowiring failure. Type-hint the bundle's interface (or use the named alias) and add an explicit bind in services.yaml if needed.
  • Missing keys fail silently. A typo in order_status.pending returns the key string, which renders as a plausible-looking label in a dropdown until someone notices. The CI loop above is the cheapest guard.
  • Patch-version breaking changes. 1.0.11 dropped PHP < 8.3 and Symfony < 6.4 under a patch number. Treat composer update on this package as a deliberate act: pin an exact version, read the tags, and assume the next release may move the floor again.
  • Adoption risk. Single-digit stars, no dependents, and a release cadence that looks automated. Keep any patches in a fork you control, and don't build deep abstractions on top of it. On a Symfony 6.4+/PHP 8.3 baseline — now the bundle's own floor — native backed enums implementing TranslatableInterface cover this core use case with no third-party dependency and no annotation reader to lose.
  • Extension points worth knowing. The annotation driver and translation driver are swappable services. With the annotation dependency gone and the framework pushing attributes, writing a small attribute-based driver is now clearly more productive than propping up the annotation one.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core