modera/backend-translations-tool-bundle
Modera Backend Translations Tool Bundle provides a backend interface for managing translations in PHP applications. Browse, edit, and organize translation messages from one place, helping teams streamline localization workflows and keep multilingual content consistent.
First, the reality check: despite the "php package" cataloguing, modera/backend-translations-tool-bundle is a Symfony 2/3-era bundle built for MJR (Modera Junior Runtime), Modera's in-house admin framework. It has no service provider, no config publishing, no Blade/Livewire views, and no Eloquent models — there is nothing to composer require into a Laravel app. It has been read-only since 2016-06-17, with 0 stars, 0 dependents, 0 releases since 2016. As a Laravel developer, you "leverage" this package almost exclusively as prior art: a reference implementation of a translation-editing admin screen you will rebuild natively.
Where to look first (read-only, on GitHub, not in your vendor/):
ModeraBackendTranslationsToolBundle.php — the bundle class; shows which MJR sections it registers into the backend menu.Resources/config/services.xml — the services that actually read and write translation catalogues. This is where the real logic lives; everything else is UI plumbing.Resources/config/routing.yml — the JSON-RPC endpoints the ExtJS grid hits (list, filter, save, import/export).Resources/public/js/... — the ExtJS grid/editor. Skim for the field set and workflow; ignore the implementation — it is not portable to Livewire/Inertia/React and is hostile to mobile.If you are inheriting a legacy MJR/Symfony app (the only scenario where you install it), the minimal steps are:
composer require modera/backend-translations-tool-bundle
Register it in AppKernel::registerBundles(), after the MJR core and the sibling translations bundle it depends on:
new Modera\BackendTranslationsToolBundle\ModeraBackendTranslationsToolBundle(),
Clear the Symfony cache, log into the MJR backend (/backend), and look for Tools → Translations in the left nav. If the menu item is missing, the sibling ModeraBackendTranslationsBundle (the actual storage/extraction logic) is not installed — this bundle alone is presentation only.
First use case for a Laravel developer: treat the screens as a requirements checklist for a small in-house tool. The realistic first use is answering "what should our translation admin contain?" — scoping by bundle/domain, per-locale coverage views, missing-key detection, inline edit, import/export, cache-bust on save. Then budget a one- to two-sprint build over spatie/laravel-translation-loader (DB-backed) or a thin CRUD over your lang/ files.
Since you cannot install this on Laravel, the useful "patterns" are the contract and UX the bundle encodes, which you reimplement natively.
1. The catalogue grid contract. The bundle organises translations by the tuple (domain, locale, key). That three-part composite key is the right shape for your own translations table:
Schema::create('translations', function (Blueprint $t) {
$t->id();
$t->string('domain')->default('messages'); // bundle's "domain" / namespace
$t->string('locale', 8);
$t->string('key');
$t->text('value')->nullable();
$t->timestamps();
$t->unique(['domain', 'locale', 'key']);
$t->index(['locale', 'domain']);
});
This mirrors the bundle's filter-by-domain-and-locale UI, and it is the shape the missing-key detection and coverage views run against.
2. List → filter → edit → save → publish/clear-cache. The bundle's workflow is exactly this five-step loop, driven over MJR's JSON-RPC endpoints. In Laravel this maps cleanly to a Livewire or Inertia screen backed by a thin JSON controller. The "publish/clear cache" step is the one people forget — in Symfony the bundle leans on the framework cache warmer; in Laravel you must explicitly bust cache on save (see Gotchas).
3. Missing-key detection by scanning source files. The bundle scans templates and source files for translation keys and flags the ones absent from the catalogue. Reimplement directly with a queued job or Artisan command:
// Rough shape: scan app/ and resources/views for __('...') / @lang('...')
$found = collect(File::allFiles(resource_path('views')))
->flatMap(fn ($f) => preg_match_all("/@lang\\(['\"]([\w.\-]+)['\"]/", $f->getContents(), $m) ? $m[1] : []);
$missing = $found->diff(Translation::forLocale($locale)->pluck('key'));
Run this on a schedule, not on page load — the scan cost grows with the source tree and belongs out of the request cycle.
4. Per-locale coverage views. The bundle's "unified multi-locale overview" is the single most useful thing it ships: a matrix of keys × locales with present/missing cells. This is what makes "can we ship locale #4?" answerable without a developer. Build it as a query over the table above (a COUNT of expected vs. present per locale), not as an in-memory ExtJS store.
5. Import/export. The bundle supports exchanging catalogue files. On Laravel, wire this to php artisan lang:import / lang:export operating on YAML or PHP-array files from lang/, or CSV for translators. Always round-trip through the same key normalisation as the grid, or import/export will drift from what the UI shows.
6. Integration tip — pick one source of truth. The bundle assumes the Symfony catalogue is authoritative and writes back to it. If you build natively, decide up front: files in lang/ (simple, git-reviewable, but no edit-without-deploy) or a translations table (edit-without-deploy, but you need cache-busting and a sync story). Do not run both as writable sources.
composer install --ignore-platform-reqs into a production image to "try it" — you will pull in unpatched transitive dependencies into an authenticated write path. If you must spike it, do it in a throwaway container and expect failure.ModeraBackendTranslationsBundle, MJR itself) that are not pulled in by this repository alone. If you register it and see no Translations menu, the siblings are missing — and those siblings may no longer resolve on Packagist. The read-only split means issues and PRs are not accepted here.DB::transaction(fn () => $t->update($data));
Cache::forget("translations.{$locale}.{$domain}");
Artisan::call('cache:clear'); // or a targeted forget, on the write path
spatie/laravel-translation-loader or a translations table, make the DB authoritative and keep files as seed/export only.How can I help you explore Laravel packages today?