toovalu-github/translation-form-bundle
translation-form-bundle is a Symfony bundle for building translation-aware forms, offering reusable form types and configuration to manage localized content in one place. It simplifies adding multilingual fields to your forms without custom glue code.
First, the framing: toovalu-github/translation-form-bundle is a Symfony bundle built on the Symfony Form component. It will not run inside a Laravel app — there is no service provider, and it depends on symfony/form, symfony/options-resolver and Twig form themes. If you're evaluating it from a Laravel codebase, treat it as a reference implementation for the "one sub-form per locale, rendered as tabs" pattern rather than a dependency you can composer require.
For a Symfony project:
composer require toovalu-github/translation-form-bundle (check composer.json for the Symfony version constraint before committing — see Gotchas).config/bundles.php (Toovalu\TranslationFormBundle\TranslationFormBundle::class => ['all' => true]); Flex may not auto-register it since it's a low-traffic package.translation_form: in the README, and confirm the exact key names in src/DependencyInjection/Configuration.php, which is the authoritative source when docs are thin.src/Form/Type/ for the exposed form types and their configureOptions(), then tests/ — for a package with 0 stars, the test fixtures are usually better documentation than the README.First use case: a Product entity with a ProductTranslation child entity (one row per locale). You have a ProductTranslationType for a single translation and want an admin form with a tab per locale instead of hand-rolling a CollectionType keyed by locale index. That is exactly the gap this bundle fills.
Tabbed locale editor. Wrap the translation collection in the bundle's type and pass the per-locale child form type as an option. Each tab renders your normal ProductTranslationType — so your field definitions and constraints stay in one place and remain reusable outside the admin.
Always define a dedicated translation form type. Do not reuse the parent entity's type for the translation child; translated and untranslated fields diverge quickly (slug generation, meta fields), and mixing them makes the tabbed layout confusing.
Admin integration. In EasyAdmin, register it as a custom form type on the field; in Sonata, as a custom form type in configureFormFields. Because it is a Form type and not a field type, expect to do a little wiring in the admin layer rather than getting it for free.
Controller/entity wiring. The parent must own the collection with cascade: ['persist', 'remove'] and orphanRemoval: true on the Doctrine side, otherwise submitting the form saves nothing. Handle the form normally — $form->handleRequest($request), isValid(), flush() — the bundle only touches the form layer.
Locale fallback and required locales. Model "this locale is mandatory, that one is optional" as validation groups on the translation entity's constraints, not via required => true on the widget. Empty optional translations should be allowed to exist as blank rows or be pruned on preFlush; pick one and be consistent.
Non-HTML consumers. For an API, skip the form type entirely — but you can still reuse the bundle's locale list by injecting the same configuration parameter, so the UI and API agree on what "supported locales" means.
['en', 'fr'] but the entity's translations are keyed 0, 1, 2, submitted data silently lands on the wrong row. Use CollectionType by_reference => false and an index-by-locale entry_options, or verify the bundle already does this in its type.preFlush listener or accept the noise deliberately.cascade: ['persist'] on the inverse side of the association — the form binds fine and Doctrine drops the children.composer.json for the symfony/form constraint and pin it; if you're on a newer Symfony LTS than the constraint allows, vendor the form type into your app rather than fighting the resolver.configureOptions() — locale list, child form type, whether to render tabs or an accordion, and required-locale options. Read that method before forking anything.dump($form->get('translations')->getData()) after submit() but before isValid() — you'll immediately see whether the failure is in the form mapping or in Doctrine.How can I help you explore Laravel packages today?