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

Translation Bundle Laravel Package

netbull/translation-bundle

A Symfony bundle for managing translations in one place, with database-backed storage and tools to edit, organize, and serve multilingual content. It simplifies maintaining translations across your application while keeping language workflows tidy.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. Before you write a single line of app code, decide which of two paths you're on:

  1. You actually have a Symfony service in your stack (or a standalone Symfony app alongside Laravel) — use it directly.
  2. You're a Laravel shop evaluating it for ideas — read it as a reference implementation and port the ~200 lines that matter, rather than trying to shoehorn a Symfony bundle into config/app.php.

Assuming path 1, the minimal sequence:

composer require netbull/translation-bundle

Register the bundle in config/bundles.php (Symfony Flex may do this automatically if the bundle ships a recipe — verify, since with 0 dependents there is little real-world evidence a recipe exists):

Netbull\TranslationBundle\NetbullTranslationBundle::class => ['all' => true],

Publish/inspect the bundle config (php bin/console config:dump-reference netbull_translation) and create the translation table. Because translations live in Doctrine, you need a migration:

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate

Where to look first: the bundle's Resources/config/ directory (service definitions tell you what the DB loader replaces) and its Entity/ folder (the translation record shape — typically key, locale, domain/message domain, translation, updatedAt). Those two tell you 80% of the contract.

First real use case: CMS/editorial strings. Let marketing change the homepage headline, SEO meta, and email copy without a deploy. Keep developer-facing strings (validation.required, flash messages) in files; move only content-owned strings to the database. That split is where this class of bundle actually earns its keep.

Implementation Patterns

Layered translation source (the core pattern). Don't replace Symfony's file-based translator — layer the DB loader after it so files remain the canonical source for developer strings and the DB overrides/extends them. Concretely, you want a translation resource/loader that reads from the repository and returns a MessageCatalogue; Symfony then merges catalogues by locale/domain. Configure precedence explicitly and write a test that asserts a file-defined key still resolves when the DB has no row for it.

File ⇄ DB round-trip workflow. The healthy workflow for this kind of bundle is:

edit in admin UI → DB  →  translation:export → xlf/yaml in git → review → deploy
import from files → DB (seed/bootstrap)

Wire the export into CI or a nightly job so DB content is never the only copy. If the bundle ships no export command, write one in your app — it's a 30-line Command that iterates the repository and dumps MessageCatalogue to XLIFF. This single decision is what makes the dependency reversible.

Repository as the integration seam. In your own services, depend on the bundle's translation repository interface (or a thin wrapper of your own), never on the concrete entity. That lets you add caching, add a tenant_id scope, or swap the storage later without touching call sites.

Cache warming and invalidation. DB-backed translations are slow (a query per locale/domain) — that's why the bundle exists with a cache layer. The pattern that works:

  • Warm translation caches during deploy (cache:warmup after migrations).
  • Invalidate the specific locale/domain on save via an entity listener or a service-level save() that clears the catalogue.
  • Never rely on "it just works" after an admin edit; verify with php bin/console debug:translation <locale> immediately after saving.

Admin editing surface. Point your own EasyAdmin/Sonata CRUD at the entity, or use the bundle's controller if present. Whatever you do, restrict it to ROLE_ADMIN via access_control, validate that key matches ^[a-z0-9_.-]+$, and show the source string next to the translation field — translators working blind produce garbage.

Per-domain splitting. Keep messages, validators, emails, and admin in separate domains. Bundling everything into one domain makes the cache huge and the admin UI unusable past a few thousand keys.

Deploy script shape:

php bin/console doctrine:migrations:migrate --no-interaction
php bin/console netbull:translation:import   # if you ship files → DB
php bin/console cache:clear
php bin/console cache:warmup

Laravel equivalent, if you're porting. The translatable pattern is identical: a translations table (locale, group, key, value, unique on the triple), a custom LoaderInterface implementation registered via app('translation.loader'), and a Translator that consults the repository before the file loader. In Laravel you'd register it in a TranslationServiceProvider and clear the translation cache tag on save.

Gotchas and Tips

License is the headline risk. AGPL-3.0 is a strong copyleft with a network-use clause. If this bundle is served as part of a SaaS or any network-accessible product, AGPL obligations arguably extend to the combined work. For a typical commercial Laravel/Symfony SaaS this is a legal-review-level blocker, not a "note in the README" item. Get sign-off before it goes anywhere near production.

Effectively unmaintained. Zero stars, zero dependents, and a maintainer with no visible community around it means: no security response, no Symfony 8 compatibility, no one to file a bug with. Treat it as vendored source you now own. Fork it into your org, pin the commit, and budget time for Symfony version bumps yourself.

Symfony version constraints will bite. Check composer.json require before you plan anything — bundles of this vintage often pin symfony/framework-bundle: ^5|^6 and Doctrine ORM 2.x. Symfony 7+ / ORM 3 migrations (Doctrine\DBAL\Connection → ConnectionInterface, EntityManagerInterface signature changes) frequently break these.

Duplicate-key and encoding traps. The natural unique index is (domain, locale, key). Translations edited concurrently, or a key pasted with a trailing space from a spreadsheet, produce UniqueConstraintViolationException. Normalize key on write (trim, no BOM), and make the admin form enforce uniqueness before the DB does. Also watch collation: utf8 vs utf8mb4 on the translation column will silently mangle emoji.

Stale cache is the #1 "the bundle is broken" report. After editing a string in the DB, debug:translation will happily show the old value until you clear the translator cache. Add cache:pool:clear cache.app to your admin save flow, or make the save listener do it.

Locale fallback confusion. Symfony resolves fr_CA → fr → fallback_locale. A row stored under fr_CA with an empty translation will override a populated fr row with an empty string rather than falling back. Guard against empty-string saves — treat null and '' differently, or delete the row instead of blanking it.

Don't put developer strings in the DB. Keys like validation.required belong in files under code review. Once they migrate to a DB row, a git grep no longer finds their usage, dead keys accumulate, and your CI can't diff copy changes.

Extension points to prefer over patching. Before you edit vendor code, look for: a custom Doctrine repository (setRepositoryClass), an event subscriber on postPersist/postUpdate, and the loader service definition you can decorate via decorates:. Decorating the loader is the clean way to add tenant scoping or a second-level cache without forking.

Debugging checklist, in order: debug:translation <locale> <domain> (does the catalogue contain the key at all?) → debug:container <loader-id> (is the DB loader actually registered and winning?) → Symfony profiler's Translation panel (which catalogue served the string?) → raw SQL against the table (is the row there with the right locale?). That sequence isolates loader-vs-data-vs-cache in four steps.

Exit strategy, decided on day one. Write the export-to-XLIFF command before you import a thousand rows. If the project stalls in six months, exporting back to files and dropping the bundle is a one-afternoon job — and that is the only thing making a 0-star, AGPL dependency palatable.

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