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

Filament Translation Manager Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and publish. Require the package, then publish its config and migration stubs. Set translation-manager.table before migrating — the Translation model and every migration stub read that key, so changing it later means renaming tables by hand.
  2. Run the stubs you need. The base stub creates language_lines; the make_language_lines_keys_binary stub exists to convert an existing table so group/key are utf8mb4_bin. On MySQL/MariaDB run it — the scan compares keys byte-exactly, and without the binary collation the unique pair disagrees with that comparison. The add_status_columns_to_language_lines_table stub adds confirmed_at/orphaned_at and is only useful if you keep status.enabled on.
  3. Open the gates. manageTranslations and deleteTranslations are closed unless you define them. In AuthServiceProvider:
Gate::define('manageTranslations', fn (User $u) => $u->is_admin);
Gate::define('deleteTranslations', fn (User $u) => $u->is_admin);
  1. Look first at the Translations resource (the table with one column per configured locale, search, "Missing in" filters, edit/create forms). Then run your first scan:
php artisan translation-manager:scan --dry-run

--dry-run prints what would be inserted before anything is written. Once the summary looks sane, run it for real — scanning is additive and never overwrites texts an editor has already edited, so the first run on a mature app is safe.

The first genuinely useful case is almost always: developer adds __('checkout.payment_failed') in code → scan → editor finds it in the "Missing in" filter → translates it in the panel → it renders. From then on the scan is your CI/routine step.

Implementation Patterns

Scan-driven workflow (the main loop). Put translation-manager:scan in your deploy pipeline (or a scheduled job) and have it run after the cache is cleared. Locally, prefer --dry-run in a pre-commit check so a missing key shows up before merge. The scan understands __(), trans(), trans_choice(), @lang and Lang::get(), plus PHP lang files and JSON groups — one shared literal parser, so dynamic keys (__("foo.$bar")) are simply not found. Those need to be created by hand.

Prune is non-destructive by default. --prune in prune.mode = mark sets orphaned_at on keys the scan no longer finds, and clears the mark on any scan that does find the key. This is what you want in a normal pipeline: it turns "dead translations" into a filterable list ("Not found in code") rather than a delete. Only switch prune.mode to delete for a one-off cleanup, and run --prune --dry-run first.

Use the status workflow to close the loop. confirmed_at means an editor has vouched for the row — confirmed rows are never reported "missing", so a legitimately-unscannable key (built dynamically, or a key you intentionally keep) can be confirmed once and stop generating noise. Combine with missing.value_equal_to_key = true so half-finished stubs where the text is just the key still surface as missing.

Import/export for handing work to translators. Export per language, send the JSON out, import back. Import honours groups and skip_group_like_keys, and import.max_size caps the upload — set it to something meaningful (a few MB) rather than leaving it unbounded. Since import writes in chunks of 200 inside transactions with model events firing per row, any saved observers you have on Translation still work, but they will fire up to N times per file.

Scoping a multi-tenant or multi-team install. Use config groups to limit the resource list, counts, quick translate and the scan to a subset. If several teams share the table, this is your cheapest separation. Pair with skip_group_like_keys to exclude noisy namespaces (validation, pagination, package vendor groups) from the scan entirely.

Custom locale resolution. If locales live in a database table or a tenant setting rather than config('app.locales'), register:

Locales::resolveUsing(fn () => Tenant::current()->locales);

Do this in a service provider — the column set, the "Missing in" filters and the search all derive from it, and they are resolved per request.

Extending the resource without forking. Host extension points, roughly in increasing order of invasiveness:

  • navigationGroup() / navigationLabel() accept closures for menu placement.
  • TranslationManagerPlugin::resource() swaps or subclasses the resource (register it in your panel provider).
  • Resource hooks scopeQuery(), extraColumns(), extraRecordActions() let you filter rows (e.g. to a tenant's groups), add a column, or add a custom row action (e.g. "copy to staging").

Search behaviour to design around. Search matches the key and text->locale for every configured locale — not the raw JSON blob — so a search for "invoice" won't accidentally match a stray JSON fragment or another locale's metadata. The scopes compile to the query builder's JSON paths (text->uk) and are tested against the MySQL, MariaDB, PostgreSQL and SQLite grammars, so you can rely on them cross-DB. Rows in the * group have their title rendered as just the key, since JSON-group keys have no namespace.

Language switcher (opt-in). If enabled, it applies the locale through the panel's persistent auth middleware — i.e. after the session starts, not in boot(). If you write your own switcher, copy that placement; setting the locale in a service provider's boot() runs before the session and will silently not stick.

Gotchas and Tips

  • The binary-key stub is not optional on an existing MySQL/MariaDB install. Without utf8mb4_bin, the DB's uniqueness judgement and the scanner's byte comparison can disagree — you get either a duplicate the app thinks is new or a "duplicate" the DB refuses. The good news: a duplicate the database still refuses is skipped and counted, not fatal, so the scan won't explode mid-deploy. Read the scan summary — a non-zero skipped count is your signal that collation or data is off.
  • Gates closed by default is a deliberate foot-gun guard, but it's the #1 "the panel is empty/403" cause. Define both gates before you file a bug.
  • Cache flush is once per group, at the end. Scans and imports use Translation::deferringCacheFlush() and chunk in 200-row transactions. Consequence: during a long import, trans() may serve stale values until the group finishes. If you trigger a scan from a job and immediately read translations in the same request, expect the pre-scan cache. Outside the package (your own bulk inserts), call the flush yourself.
  • Model events still fire per row despite the deferred flush — so saved/created observers that themselves call trans() will see stale cache mid-run. Avoid translation reads inside those observers, or queue the follow-up work.
  • scan.max_key_length is characters, null = unlimited. A key skipped for length is not written and never orphans its row — so lowering the limit later won't retroactively mark those rows orphaned. Set it once, deliberately.
  • A cleared locale is stored as null, and the scan only fills locales a row does not have. That means an editor deliberately blanking a translation is respected forever; the scan will not "helpfully" refill it with the key. This is the correct behaviour, but it surprises people who expect the scan to re-seed everything. If you do want the key copied into every empty locale, fill_all_locales_with_key is the opt-in.
  • --prune ≠ delete. Default prune.mode = mark. Verify the mode before running anything destructive in production; delete retains the old hard-delete behaviour. Always --prune --dry-run first.
  • status.enabled = false switches off the whole status layer — badge, filters and the Confirm / Remove confirmation action — but leaves the columns alone. If you're upgrading an existing install, decide this before running the status migration stub; migrating and then disabling is harmless but adds dead columns.
  • Search cost scales with locale count. It hits text->locale for every configured locale, and JSON-path expressions generally aren't index-friendly. On a large language_lines table with 10 locales, expect a slower LIKE. Narrow with groups and add pagination-appropriate indexes on group/key rather than trying to index the JSON.
  • QuickTranslate::$recordId is now locked and the lookup scoped. This closes a tampering vector (previously a crafted Livewire payload could target a row outside your scope), but if you had custom code poking that property it will no longer take effect — set the record through the supported entry point instead.
  • import.max_size is the only upload guard. Don't rely on PHP's upload_max_filesize alone; set the package limit so you get a clean validation error instead of a silent empty import.
  • Config table is read by the model and the migration stubs. Changing it after migrating leaves your data in the old table; the package will happily query an empty new one.
  • Locales are resolved at request time from Locales::resolveUsing(). If your resolver can return a different set per tenant, the columns and the "Missing in" filters change too — make sure every tenant's rows actually contain those keys, or you'll see phantom "missing" everywhere.
  • Debugging a key that "won't scan in": first confirm it's a literal (no interpolation/concatenation), then check groups/skip_group_like_keys didn't exclude its namespace, then max_key_length, then whether the row is confirmed_at (confirmed rows are excluded from "missing"). That order covers nearly every report.
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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log