asignua/filament-translation-manager
A Filament plugin for managing Laravel translation files from your admin panel. Browse, edit, add, and organize translations across locales in a simple UI, keeping language files tidy without touching PHP arrays by hand.
spatie/laravel-translation-loader's language_lines, a code scanner, JSON import/export, a quick-translate page, and a language switcher. Building that in-house is weeks of engineering for zero differentiation. The package is schema-coupled to a library the team likely already uses, so the "buy" cost is a config file, not a data migration.orphaned_at lets you quantify dead strings ("we're carrying 1,400 keys no code references") and then decide whether to clean up. Crucially, --prune defaults to mark, not delete, so this analysis is safe to run in production and the destructive behaviour is a separate, deliberate opt-in (prune.mode=delete).confirmed_at distinguishes "a machine/key-fill put text here" from "a human vouched for this string," and confirmed rows are excluded from "Missing." That's the difference between a coverage metric you can put in a board deck and one you can't. It also gives you a per-locale reviewer workflow without buying a TMS.null and the scan never refills it; the scan is additive and never overwrites edited text. This means "run the scan" is a safe, repeatable operation you can put on a schedule or a button — not a change-freeze event.groups limits the resource, counts, quick translate, and scan to selected namespaces, and skip_group_like_keys filters out validation.*-style noise. You can roll this out to one feature area first and exclude vendor/validation strings from the editor's view.text->locale of every configured locale (not raw JSON), so a support agent can find the copy behind a screen without knowing the key naming convention. This is a small feature with outsized impact on non-engineer adoption.config('table'), Locales::resolveUsing(), TranslationManagerPlugin::resource(), navigation closures, and the scopeQuery() / extraColumns() / extraRecordActions() hooks decide whether this lives inside the existing admin panel and inherits its permissions, or sits beside it. That's an architecture call the PM should be in on, because it determines whether editors need a second login.manageTranslations and deleteTranslations gates are closed unless you define them, and delete is gated. Translation text is user-facing copy with SEO and legal exposure; "no one can mass-delete strings by accident" is a launch requirement, not a nicety.Consider adopting when:
spatie/laravel-translation-loader and its language_lines table. The package is explicitly built on that schema; the migration stubs read config('table'), so a rename is supported but a different schema is not.__(), trans(), trans_choice(), @lang, Lang::get() and lang-file string), the missing/orphaned views, and the editor UI — this is overhead for a single-locale app with 80 strings.language_lines table — the make_language_lines_keys_binary stub retrofits utf8mb4_bin onto it. This is the single most valuable migration in the release: it makes the unique (group, key) pair agree with the scanner's byte-exact comparison, which is exactly the class of bug (two keys that look identical, one silently shadowing the other) that erodes trust in a translation tool.Look elsewhere when:
confirmed_at is a single flag, not an audit trail.lang/ only) translations with no database — the scan reads lang files, but the product is the language_lines resource.Adoption caveats to plan for (v1.0.0):
import.max_size and decide scan.max_key_length deliberately — null (unlimited) is the default and permissive.status.enabled lets you ship without the confirmed/orphaned columns at all. That's a useful phased-rollout lever: turn on the resource first, add status once editors are used to the tool.To executives: This is the last piece of tooling that stands between us and launching in a new market without an engineer in the loop. Today every copy change is a ticket. With this, editors see exactly which strings are missing for Polish or Turkish, translate them in the admin panel we already run, mark them confirmed, and the strings ship — no deploy, no developer. It adds no SaaS line item and no new vendor; it sits on the translation table we already have. The safety story is deliberately conservative: destructive pruning is off by default, deletes are permission-gated and off until we turn them on, and the scanner is additive — it never overwrites text a human edited, and it never refills a field an editor deliberately cleared. Ask: a two-week pilot on one surface and one locale, with a coverage target we agree up front; we report coverage and orphaned-key counts at the end and decide on full rollout.
To engineering: It's a Filament resource plus an artisan scanner over spatie/laravel-translation-loader, so no new schema to design and no second source of truth. The implementation shows real care about the things that bite: group/key are utf8mb4_bin so the unique index agrees with the scanner's byte comparison (with a stub to fix an existing table); duplicate collisions are skipped and counted rather than fatal; scan and import write in 200-row transactions with the cache flushed once per group via deferringCacheFlush() rather than per row, while model events still fire per row; the language switcher applies the locale after the session starts via the panel's persistent auth middleware instead of in boot(); quick translate locks $recordId and scopes the lookup; imports are size-capped; and the missing-scope queries use JSON paths (text->uk) verified against the MySQL, MariaDB, PostgreSQL and SQLite grammars. The obvious review work is the one-time collation migration on a copy of production — everything else is config. Ask: one engineer-day to pilot it, stage-test the binary-keys migration, and set groups, gates, import.max_size and scan.max_key_length to our conventions. If it holds, it's a config entry in the panel provider and the team stops hand-editing translation rows.
How can I help you explore Laravel packages today?