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

Object Translation Bundle Laravel Package

pms-nz/object-translation-bundle

Object Translation Bundle is a PHP package for attaching locale-specific translations to objects, making it easier to serve localized content without duplicating model logic. It provides straightforward storage and retrieval of translated object values for multilingual applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle (pms-nz/object-translation-bundle), not a Laravel package — it has no service provider, no config/object-translation.php, and it leans on Doctrine entities plus the Symfony DI container. As a Laravel developer you will realistically meet it in one of two situations: you're maintaining a Symfony/Doctrine service, or you're running Doctrine inside Laravel and are prepared to wire the container definitions yourself. If you're on Eloquent, stop here — the concepts (per-locale rows, a translation manager, a locale-aware hydrator) are worth copying, the code is not.

Minimal path for a Symfony/Doctrine app:

composer require pms-nz/object-translation-bundle:^1.0.3
  1. Register the bundle in config/bundles.php (PMS\ObjectTranslationBundle\... => ['all' => true]).
  2. Point the config at your translation entity. The bundle expects a single translation_class that serves as the storage shape for every translatable object.
  3. Read two files first, in this order:
    • AbstractTranslation — the base class your translation entity is expected to extend. Every method you'll call at runtime (getObject(), getLocale(), setLocale(), the field accessors) comes from here.
    • The Translatable contract / trait — this is what you implement on the translated entity (the one users see), and it's what gives you the familiar $post->translate('fr')->getTitle() accessor.
  4. First use case: a Post entity with title and body that must render in en, fr, and mi. You keep Post free of locale columns, add a single PostTranslation extends AbstractTranslation, and let the bundle resolve the right row from the request locale with a fallback.

Before you write a line of your own code, do step 3's check manually — see the first Gotcha, because v1.0.3 removed the safety net that used to catch this for you.

Implementation Patterns

The two-entity shape. One "object" entity (Post), one "translation" entity (PostTranslation). The translation entity holds every localised column plus locale and a reference back to the object. The object entity implements the translatable contract and exposes a translations collection. This is the only shape the bundle understands; don't try to be clever with a polymorphic single-table design unless you're prepared to own all the query building.

Locale resolution. Typically you set the current locale once per request (from the request, a user preference, or a route parameter) and the manager resolves translations from there, with a configured fallback locale for missing rows. The pattern that holds up:

// per-request, before rendering — never inside an entity
$translationManager->setLocale($request->getLocale());
$translationManager->setFallbackLocale('en');

Saving. Cascade-persist the translations collection from the parent so a single flush() covers it, and always set the locale on the translation object yourself before adding it — the bundle will not guess it from the object's context.

Querying for a locale. Joining the translation table with a locale predicate is the fast path and avoids the per-object lookup the manager does lazily:

$qb->join('p.translations', 't', 'WITH', 't.locale = :locale')
   ->setParameter('locale', $locale);

Forms / CRUD. Two workable layouts: a tab-per-locale UI that swaps the bound translation object, or a CollectionType over translations with a fixed set of locales. The collection approach tests better because a single submit exercises every locale at once.

Testing. Boot the kernel in a functional test, create fixtures in two locales, and assert both the hit and the fallback path. Add one test that asserts your translation entity is an instance of AbstractTranslation — that test is now your only install-time guard (see Gotchas).

Gotchas and Tips

The v1.0.3 change is a removal of a guard rail, and the changelog wording is genuinely confusing. It reads "Remove install requirement that the translation_class should extend AbstractTranslation. This requirement must be met before this bundle can be used." Read it as: the automated check is gone; the requirement itself is not. Your translation class must still extend AbstractTranslation or you will hit fatal errors on missing methods once the manager starts hydrating. The difference is when it fails — previously at install/container build, now somewhere deep in a request. Add a cheap test:

public function testTranslationClassFulfilsContract(): void
{
    $class = $this->container->getParameter('pms_object_translation.translation_class');
    $this->assertTrue(is_subclass_of($class, AbstractTranslation::class));
}

If you maintain a CI pipeline, this is the single highest-value line to add when upgrading. Without it, a misconfigured class goes from "composer install fails loudly" to "production 500 on the first translated page".

Upgrade risk is otherwise low. 1.0.2 -> 1.0.3 is a patch removal of a validation check; there is no API change, no deprecation, no migration. Pin ^1.0.3 and move on. The failure mode is purely "bad configuration no longer caught early."

Locale leakage in long-running processes. The manager is a shared service. Under a worker, long-running Octane/Swoole-style process, or a queue daemon, the locale set during request A carries into job B. Reset it explicitly at the start of every job/request rather than relying on it being initialised.

N+1 on translations. Lazy resolution through the manager issues one query per object. On any index page, eager-join translations for the current locale in the query builder; otherwise a 30-row listing becomes 31 queries. This is the most common performance complaint with this pattern, and it's entirely avoidable.

Fallback vs empty string. A translation row that exists but has an empty title will not trigger the fallback — the row is present, so the value wins. If editors in non-primary locales legitimately leave fields blank, decide whether you want null (falls back) or '' (renders blank) and enforce it in the entity setter.

Cache invalidation after bulk imports. Any translation import that writes rows directly (not through the manager) leaves stale resolved objects behind for the rest of the request and any cache layer. Run imports as separate processes and clear the relevant caches after.

Extension points. The translation manager and locale resolver are ordinary services — decorate or replace them via services.yaml rather than subclassing the entity. Overriding the entity is a dead end; overriding the resolver is where per-tenant locales, header negotiation, or admin-preview-in-another-locale logic belongs.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle