ladela/personal-translations-widget-bundle
Ladela PersonalTranslationsWidgetBundle adds a ready-to-use Symfony widget for editing personal translations in your admin interface. Manage localized content alongside your entities and integrate translation editing into existing workflows with minimal setup.
This is a Symfony bundle, not a Laravel package — there is no service provider, no config/personal-translations-widget.php, and nothing to php artisan vendor:publish. As a Laravel developer your realistic options are (a) read it as a reference implementation for a per-locale translation editing widget, or (b) use it only if you're working inside a Symfony app. The evaluation below covers both angles.
What it actually does. It layers a form widget on top of the "personal translations" approach to Doctrine i18n (one row per object + field + locale, rather than Gedmo-style translated columns). The bundle supplies a form type / form type extension plus Twig templates that render translation fields as a tabbed, per-locale editor inside a normal Symfony form.
Minimal steps in a Symfony app:
composer require ladela/personal-translations-widget-bundle
config/bundles.php (or AppKernel::registerBundles() on older Symfony).$builder->add('translations', PersonalTranslationsWidgetType::class, [
'locales' => ['en', 'de', 'fr'],
]);
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_widget(form.translations) }}
{{ form_end(form) }}
Where to look first: the Form/Type directory (the widget type and any FormTypeExtension), then Resources/views (tab markup and the form theme block), then DependencyInjection/Configuration.php for the locale/options surface. The README is thin — the source is the documentation.
First use case: an admin CRUD screen where an editor fills in a product/category/page title and body once per locale, in tabs, and saves the whole thing with one submit.
Laravel equivalent for context: a Blade partial that loops config('app.locales') into Bootstrap tabs, a hasMany translations relation, and an updateOrCreate(['locale' => $l], [...]) loop in a form request or an action class. That's roughly 60 lines — worth knowing before you invest in porting anything.
Tab-per-locale inside a single form. The dominant pattern is one submit button, N locale panels. The widget handles the loop; you supply the locale list from config so it stays in sync with the rest of the app:
# config/packages/personal_translations_widget.yaml (shape will vary by version)
personal_translations_widget:
locales: [en, de, fr]
Pair it with the persistence listener. The widget only renders inputs. Values are written because the personal-translations bundle's Doctrine listener/subscriber picks up the mapped translation collection on flush. If you render the widget but the underlying mapping isn't wired, you get a pretty form that silently discards input.
Embedding in admin generators. The clean integration point is a custom form type wrapping the widget, then referencing that type from Sonata Admin / EasyAdmin / a hand-rolled controller. Keeps the widget options (locales, required locale, tab style) in one place instead of repeated in every admin class.
Validation per locale. Attach constraints to the translation entity itself (NotBlank on the default locale, Length on others) rather than to the parent form — otherwise a single global required flag makes every locale mandatory, which is rarely what editors want.
Template override for styling. Copy the bundle's view into templates/bundles/PersonalTranslationsWidgetBundle/... and restyle. Expect the shipped markup to assume a specific CSS framework (Bootstrap-era tabs and a JS dependency); the override is usually the first thing you do.
Laravel-side pattern if you're porting the idea: keep translations in a translations table with a unique index on (translatable_type, translatable_id, locale, field) or a single JSON column. Use a Translations value object with get($locale, $fallback) and hydrate it in an accessor. Render tabs with an Alpine.js x-data="{ tab: 'en' }" — no jQuery, no bundle.
ladela/ vendor prefix over a rodgermd/ repo — a fork-of-a-fork. Pin an exact version in composer.json, commit the lock, and assume you own the code. Do not build a roadmap on it.getName() vs getBlockPrefix(), Resources/views vs templates/, AppKernel vs bundles.php) place this in the Symfony 2/3 era. Expect to patch method signatures and move templates before it runs on anything modern.composer.json require before you plan anything.cascade={"persist"}.bin/console debug:event-dispatcher and check the SQL log for INSERT INTO ..._translations.{% form_theme form '...' %} in some setups; without it you get an "unknown form block" Twig error rather than a fallback render.bin/console debug:container --tag=form.type, debug:twig for the template path, and the profiler's Forms panel to confirm the widget type is actually attached to the field.How can I help you explore Laravel packages today?