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 Editor Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. MongoDB → Doctrine ORM. This is the interesting signal. The editor was never purely file-backed; it has a persistence layer, and that layer just swapped storage engines. That tells you the author kept the interface stable while replacing the driver — which is exactly the seam worth copying into Laravel.
  2. Import/Dump on any translation file. The genuinely portable feature. Bidirectional conversion between formats (PHP array, JSON, YAML, XLIFF) is useful on its own, with or without the editor UI.
  3. "Added support for new keys." The key-discovery matrix got work. Verify in source whether that means orphaned keys, unused keys, or new-key creation in the UI.
  4. "Heavy changes on original code." Not a changelog. Treat every claim as unverified until you read the diff.

Where to look first (revised priority):

  1. composer.json — confirm it's still Symfony 2.x/3.x bounds and the PHP floor; v1.0.0 may have moved the floor.
  2. The new import/dump services — this is the highest-value code in the repo now. Find the reader/writer pairs and the canonical intermediate representation they share.
  3. The Doctrine entity + repository layer — shows the schema the editor assumes (locale, domain, key, value, timestamps). Map it to a Laravel migration if you go DB-backed.
  4. DependencyInjection/ + Resources/config/services.xml — the wiring now includes Doctrine services, so the container graph got heavier, not lighter.
  5. Controller/ and Resources/config/routing.* — the CRUD surface and URL shape to mirror in Laravel routes.
  6. The loader/writer layer — unchanged in spirit: find files → parse into a locale × domain × key matrix → write back in place.

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.

Implementation Patterns

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:

  • Run 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.
  • Preserve insertion order — require returns ordered arrays, so merge rather than ksort. Order churn is the #1 source of unreviewable translation diffs.
  • Fire a 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.
  • Put bulk imports behind SaveTranslationJob. v1.0.0's dump/import makes 2,000-key payloads routine; keep them off the request cycle.
  • If you adopt a DB store, write translations to the DB and export to lang/ on deploy via artisan translations:export. Runtime should never depend on the DB for lookups.

Gotchas and Tips

  • Still not installable, and now harder to read. v1.0.0 adds Doctrine ORM wiring on top of the Symfony DI graph, so the container config is heavier than the 2013 version. Skim services.xml for what the editor needs, then close the tab — don't try to run it.
  • "Heavy changes on original code" is not a changelog. No migration guide, no upgrade notes, no tagged diff narrative. Assume breakage between the pre-1.0 tag and v1.0.0 and verify each claim against source before relying on it.
  • Dropping MongoDB is a hard break for anyone who ran it. There is no provided data migration from Mongo documents to Doctrine rows. If you had production data in the old store, writing that migration is on you — another argument for keeping storage behind your own interface.
  • Doctrine entities imply schema you must mirror. If you port the DB pattern, you inherit migrations, indexes on (locale, domain, key), and a uniqueness constraint across those three. Get the constraint right or you'll silently duplicate keys.
  • Placeholder syntax is not portable. Symfony uses %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.
  • Import is an RCE-adjacent surface. Dumping to PHP generates executable files. Validate file extensions against an allowlist, refuse paths outside lang/, and never accept a user-supplied output path.
  • Never expose the editor publicly. auth + can:edit-translations + App::environment() + optional IP allowlist. The bundle relied on the Symfony firewall; you have no such default.
  • Atomic writes and opcache. 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.
  • Escaping and key types. 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.
  • Missing-key false positives are the norm. Dynamic keys (__($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.
  • Git noise is the real cost. One file per locale/group, stable formatting, and reviewers who only read added lines. A UI that rewrites whole files per save produces unmergeable diffs across branches — the fastest way to get the editor banned by your team.
  • Cache surprises. 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.
  • Extension points worth keeping in a port: format drivers (reader/writer per format), a storage driver interface, a pre-save validator (placeholder/ICU consistency, no empty values), and a post-save event. Those four cover localization-team workflows without the bundle's container gymnastics.
  • When to skip building it: more than ~2 locales, or any non-engineer translators, means a SaaS (Crowdin/Lokalise/Phrase) plus artisan translations:pull beats a homegrown editor. Take the import/dump command from this package's design and leave the rest.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console