instaclick/translation-editor-bundle
instaclick/translation-editor-bundle is a Symfony bundle that provides a web-based editor for managing application translations. Browse, edit, and save translation messages from a simple UI, streamlining localization workflows without manually touching translation files.
v1.0.0 landed — the verdict is unchanged. instaclick/translation-editor-bundle is still a Symfony bundle: no service provider, no facade, no publishable config/, and its symfony/* constraints still collide with Laravel 10/11/12 on PHP 8.2+. Do not composer require it. The release notes describe internal Symfony work (MongoDB out, Doctrine ORM in, UI fixes, import/dump), not a framework port. Treat it — still — as reference architecture, and read the new code for the ideas it now exposes.
What actually changed in v1.0.0, and why you should care:
Where to look first (revised priority):
composer.json — confirm it's still Symfony 2.x/3.x bounds and the PHP floor; v1.0.0 may have moved the floor.DependencyInjection/ + Resources/config/services.xml — the wiring now includes Doctrine services, so the container graph got heavier, not lighter.Controller/ and Resources/config/routing.* — the CRUD surface and URL shape to mirror in Laravel routes.First use case to port now: the import/dump path, not the coverage page. A single translations:convert Artisan command that reads lang/{locale}/*.php and lang/{locale}.json into one canonical array and dumps to any target format (JSON for translators, XLIFF for a TMS, PHP for shipping) is an afternoon of work and immediately useful. The read-only coverage page is still the safe second step.
Laravel-native substitutes: barryvdh/laravel-translation-manager (web UI over lang/), joedixon/laravel-translation (DB-backed + UI), Laravel Lang for shipped files. On the Symfony side, LexikTranslationBundle remains the maintained descendant of this idea — read its source, not this one.
Pattern 1 — a canonical array in the middle, drivers on both ends. This is what v1.0.0's import/dump feature legitimises, and it's the biggest improvement over the old file-only framing. Normalise everything through one shape, then let format adapters read and write at the edges:
interface TranslationReader { public function read(string $path): array; }
interface TranslationWriter { public function write(string $path, array $data): void; }
// Canonical shape: [locale => [group => [key => value]]]
Register PhpArrayReader/Writer, JsonReader/Writer, and add Yaml/Xliff only when you actually exchange with translators. Conversion becomes $writer->write($target, $reader->read($source)) — no special-case code per format pair.
Pattern 2 — keep storage behind an interface; the package just proved why. Mongo → Doctrine was a driver swap, not a rewrite. Do the same in Laravel and you keep the option open:
interface TranslationStore {
public function all(): array; // [locale => [group => [key => value]]]
public function put(string $locale, string $group, string $key, string $value): void;
public function forget(string $locale, string $group, string $key): void;
}
Ship FileStore first (it keeps lang/ as source of truth, matching how __() actually resolves). Add DatabaseStore only when you need audit history or concurrent non-engineer editing — and when you do, mirror the Doctrine schema v1.0.0 settled on: locale, domain, key, value, updated_at, updated_by. Bind the implementation in a config/translation-editor.php with 'driver' => env('TRANSLATION_EDITOR_DRIVER', 'file').
Pattern 3 — an Artisan command as the extraction/audit step. Unchanged and still the core workflow. Scan usage as well as files:
// app/Console/Commands/TranslationAudit.php
// regex over app/, resources/views/, routes/ for __(), @lang, trans(), trans_choice()
// diff used keys against TranslationStore::all() -> missing / unused / orphaned
Emit JSON so one payload feeds the Blade UI, a CI gate (--fail-on-missing), and a PR comment. v1.0.0's "new keys" work is the same problem; don't assume their heuristics are better than a 40-line regex over your own codebase.
Pattern 4 — edit-in-place overlay via middleware, gated hard. Still the bundle's best trick: show untranslated keys while browsing. In Laravel, a middleware that injects a partial into HTML responses only when Gate::allows('edit-translations') and app()->environment('local', 'staging'). Never key it off a query string or a cookie.
Pattern 5 — write-back as a diff, not a live PUT. Unchanged and non-negotiable. Production produces branch → commit → PR. Local/staging writes atomically:
$tmp = $path.'.tmp';
file_put_contents($tmp, "<?php\n\nreturn ".var_export($data, true).";\n");
rename($tmp, $path); // atomic on same filesystem
opcache_invalidate($path, true); // required if opcache.validate_timestamps=0
Integration tips:
pint after every write, and emit short-array syntax with a trailing comma per element, so generated files are byte-stable and diffs stay reviewable.require returns ordered arrays, so merge rather than ksort. Order churn is the #1 source of unreviewable translation diffs.TranslationSaved domain event after writes; hook audit logging, cache busting, and any TMS push off it. This replaces the bundle's container-level extension points with something debuggable.SaveTranslationJob. v1.0.0's dump/import makes 2,000-key payloads routine; keep them off the request cycle.lang/ on deploy via artisan translations:export. Runtime should never depend on the DB for lookups.services.xml for what the editor needs, then close the tab — don't try to run it.(locale, domain, key), and a uniqueness constraint across those three. Get the constraint right or you'll silently duplicate keys.%name%; Laravel uses :name. Copying strings from a Symfony catalogue into lang/ silently breaks __('msg', ['name' => ...]). Pluralisation differs too: Symfony's | ranges vs ICU MessageFormat vs trans_choice's {0} ... |[1,Inf] .... v1.0.0's import/dump makes cross-format copying easier to do accidentally — build the transform into the import path, not into a wiki page.lang/, and never accept a user-supplied output path.auth + can:edit-translations + App::environment() + optional IP allowlist. The bundle relied on the Symfony firewall; you have no such default.file_put_contents mid-request can be read half-written by a concurrent request; the tmp-file + rename + opcache_invalidate sequence prevents the classic "my translation reverted" bug. This bites hardest right after a bulk import.var_export($data, true) handles quotes but emits numeric keys as ints — a key like '404' comes back as 404. Cast keys to string on read. For JSON use JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT so non-ASCII stays readable in diffs.__($key)), validation attributes, vendor package translations, and dotted JSON keys all pollute the audit. Keep an ignore list in config; expect ~20% noise, and don't chase it to zero.config:cache and view:clear don't touch lang/, but any translation cache you add will. Expose translations:clear and call it from the save path and from the deploy script.artisan translations:pull beats a homegrown editor. Take the import/dump command from this package's design and leave the rest.How can I help you explore Laravel packages today?