Translation Manager by asign
CommunityEdit __() strings in the database: code scan, missing-per-language filter, quick translate, JSON import/export — successor of kenepa/translation-manager.
Author:
asign
Package health
Automated checks of this plugin's Composer package
15 checks
- Passed: GitHub Actions pinned to SHA
- Skipped: GitLab CI includes pinned to SHA
- Passed: Open security advisories
- Passed: Dependabot PR responsiveness — No open Dependabot PRs.
- Skipped: Renovate MR responsiveness
- Passed: Dependabot or Renovate configured
- Passed: Dependency update cooldown configured
- Passed: Provides a security policy
- Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
- Passed: Commit and release recency — Active: last commit 0 days ago; last release 0 days ago.
-
Passed:
composer.lock not committed by library
—
composer.lockis absent from the released dist archive. - Passed: Dist archive is lean
-
Passed:
Current Laravel version supported
—
Package dependencies resolve together with current Laravel
13.0. -
Passed:
Current PHP version supported
—
Constraint
^8.3supports current PHP8.5. -
Passed:
Current Symfony version supported
—
Package dependencies resolve together with current Symfony
8.1.
filament/
namespace. Review the source and install at your own risk. Found
malware or an unresolved security issue the author won't
address?
Report it
.
Documentation
- Screenshots
- Requirements
- Installation
- Usage
- Configuration
- Migrating from kenepa/translation-manager
- Gotchas
- Translations
- AI agents
- Testing
- Changelog
- More plugins by asign
- License
Edit every __('...') string of your Laravel app from the Filament panel, per language, without a deploy. Texts live in the
database (spatie/laravel-translation-loader) and override the lang
files, so the files stay your defaults. A scan finds the strings in your code for you, a filter shows what is still missing in a
language, and a quick-translate page walks you through the gaps one by one.
It is the successor of kenepa/translation-manager, which never shipped Filament 5, on the same storage (language_lines), the
same gate and the same config keys, so an existing project moves over without touching its data (see
Migrating from kenepa/translation-manager).
- Searchable table: group, key and one column per language. One search box looks in the key and in every text.
- "Missing in" filter (pick one or several languages) and "Missing in any language".
- Edit form with a field per language; the lang-file value is shown under it as a hint.
- Scan: finds
__(),trans(),trans_choice(),@langandLang::get()in your code, plus your lang files (PHP groups and JSON), and adds the missing rows. A text you edited is never overwritten. Also asphp artisan translation-manager:scan. - Quick translate page: the next missing text for a language, the other languages shown as the source, "Save & next".
- JSON export / import per language (the format of
lang/xx.json). - Optional language switcher in the top bar (session, and optionally a user column). Off by default.
- Closed by default: nobody opens it until you define a gate. Deleting has its own, stricter gate.
- Cyrillic and other non-ASCII text is stored readable in the database, not as
До.... - English, Ukrainian, Polish, German, French, Spanish, Italian, Dutch, Brazilian Portuguese and Turkish.
#Screenshots


#Requirements
- PHP 8.3+
- Laravel 12 or 13
- Filament 5
#Installation
composer require asignua/filament-translation-manager
php artisan vendor:publish --tag=filament-translation-manager-migrations
php artisan migrate
The migrations are publishable stubs. The first creates the language_lines table the way spatie's own migration does, and
does nothing when the table already exists, so do not also publish spatie's. The fourth (make_language_lines_keys_binary) makes the key columns case- and space-exact on MySQL/MariaDB for a table that already exists; the second adds the column --prune uses to
tell scanner-created rows from yours, the third the nullable confirmed_at / orphaned_at status columns (skip it and set
status.enabled to false if you do not want the status). Optionally publish the config (--tag=filament-translation-manager-config).
Register the plugin on your panel and tell it the languages:
use Asignua\FilamentTranslationManager\TranslationManagerPlugin;
$panel->plugin(
TranslationManagerPlugin::make()
->locales(['en' => 'English', 'uk' => 'Українська', 'de' => 'Deutsch'])
);
Then open the door. The plugin is closed until a gate exists (an undefined ability means "no"):
use Illuminate\Support\Facades\Gate;
Gate::define('manageTranslations', fn (User $user): bool => $user->isAdmin());
Gate::define('deleteTranslations', fn (User $user): bool => $user->isAdmin()); // single and bulk delete
Open Translations in the navigation and press Scan code once.
#Usage
Scan. The header action (or php artisan translation-manager:scan) reads the folders in scan.paths (default app,
resources/views, routes; tests, vendor, node_modules are skipped) and your lang files, and creates a row for every
string it has not seen. For a PHP group (messages.checkout.title) the file text lands in the row; for a JSON-style key
(__('Add to basket')) the key itself becomes the text of the default language. Languages without a text stay missing.
- It is additive: an existing text is never overwritten, a gap in an existing row is filled from the lang files.
--dry-runonly reports.--pruneacts on rows whose key is gone from the code and the lang files. By default (prune.mode=mark) it only marks them (orphaned_at, shown as "Not in code" and filterable); the mark is cleared by the next scan that finds the key again, and nothing is deleted. Withprune.mode=delete(the "Also delete rows no longer found" toggle needs the delete gate) it deletes them, by default only rows the scanner created itself (prune.only_scanned), never ones you typed. Strings built at run time (__($key),__('a'.$b)) cannot be found: review before pruning.- The scan skips what the database loader cannot serve: namespaced keys (
pkg::group.key) and keys over 255 characters. They are counted as "skipped".
Status. Every row has a badge: OK, Missing (a text is absent in a configured language) or Not in code (the last
scan did not find the key). A text that equals the key counts as missing too (missing.value_equal_to_key, on by default):
that is the placeholder a scan leaves for a string nobody translated yet. When such a text is right on purpose (a brand
name, a word that is the same in every language) press Confirm in the row menu; a confirmed row is never missing.
The language the keys are written in (missing.source_locale, default app.fallback_locale) is exempt: there the key
is the text (Collection in English). A key without a single letter (a phone number, a price) has nothing to translate:
the scan confirms it by itself.
Filters: Missing in…, Missing in any locale, Not found in code, Confirmed.
Find the gaps. Filter Missing in and pick a language, or use Missing in any language. The navigation badge
(->navigationBadge()) shows how many rows are incomplete.
Quick translate. The page walks the rows that miss a text in the chosen language, oldest first. The other languages are shown
as the source; type and press Save & next (Ctrl/Cmd+Enter works too), or Skip. ?locale=de opens it on a language.
Export / import. Export one language as xx.json: JSON-style rows by their key, group rows as group.key. Import takes the
same shape; it fills gaps and never overwrites unless you switch Overwrite existing texts on.
Language switcher. ->languageSwitcher() adds a language menu to the top bar. The choice is kept in the session and
applied when the panel boots; set language_switcher_options.user_column to a string column of your user model to remember it there as well.
To apply it on routes outside the panel too, add Asignua\FilamentTranslationManager\Http\Middleware\SetLocale after StartSession.
#Configuration
Everything is fluent on the plugin and falls back to config/filament-translation-manager.php:
TranslationManagerPlugin::make()
->locales([ // or ->availableLocales([...]) as in kenepa
['code' => 'en', 'name' => 'English', 'flag' => 'gb'], // the flag is a country code, shown as an emoji
['code' => 'uk', 'name' => 'Українська', 'flag' => 'ua'],
])
->gate('manageTranslations') // kenepa's ability was 'use-translation-manager'
->deleteGate('deleteTranslations')
->authorize(fn (User $user) => $user->isAdmin()) // replaces both gates (also accepts a bool)
->languageSwitcher()
->quickTranslate() // on by default; ->quickTranslate(false) hides the page
->navigationGroup('Settings')->navigationSort(90)->navigationIcon('heroicon-o-language')
->navigationBadge();
locales also accepts a list of codes (['en', 'uk']; the name comes from intl) or a code => name map. Without it the
plugin uses app.locale and app.fallback_locale.
Config keys: table, groups, status.enabled, missing.value_equal_to_key, missing.source_locale, locales (alias available_locales), default_locale, model, gate, delete_gate, language_switcher,
language_switcher_options.{user_column,session_key}, quick_translate.enabled, scan.{paths,extensions,exclude,include_lang_files,fill_default_locale_with_key,fill_all_locales_with_key,skip_group_like_keys},
scan.max_key_length, import.max_size, prune.{mode,only_scanned}, navigation.{group,sort,icon,badge}.
#Wrapping the plugin in a host
A host with its own table prefix, its own role system or only some of the groups configures, not forks:
config([
'filament-translation-manager.table' => 'ee_translations', // default model reads it
'filament-translation-manager.model' => App\Models\Translation::class, // may extend the plugin model
'filament-translation-manager.groups' => ['*'], // only JSON-style keys: list, scan, quick translate
'filament-translation-manager.scan.paths' => [base_path('app'), base_path('resources/views')],
'filament-translation-manager.scan.include_lang_files' => false,
]);
Locales::resolveUsing(fn () => MyLanguages::all()); // read on every call; same shapes as ->locales()
$panel->plugin(TranslationManagerPlugin::make()
->resource(MyTranslationResource::class) // or ->resource(false) when the panel discovers it
->authorize(fn () => $user->isAdmin())
->navigationGroup(fn () => __('System'))->navigationLabel(fn () => __('Translations')));
MyTranslationResource extends TranslationResource and may override canCreate(), scopeQuery(Builder), extraColumns() and
extraRecordActions(); give it its own pages (small subclasses with $resource set) when you add header actions. The model may
override getTable(); the status and "missing" logic live in scopes (missingIn, missingInAny, orphaned, confirmed,
inManagedGroups) so a host query can reuse them. JSON paths are written as text->uk and compiled by the query builder per
driver, so the same code runs on SQLite, MySQL/MariaDB and PostgreSQL (SqlGrammarTest checks the generated SQL).
#The model
Rows are served through Asignua\FilamentTranslationManager\Models\Translation, a subclass of spatie's LanguageLine. The plugin
sets spatie's translation-loader.model to it when you have not chosen another. Two reasons it is not the stock model: the stock
array cast escapes Cyrillic into \uXXXX in the database, and spatie flushes the cache only for the languages left in a row after
an edit, so clearing a language would keep serving the old text. If you use your own model, extend LanguageLine and set model.
#Migrating from kenepa/translation-manager
The data does not move: both use spatie's language_lines table. Your existing rows keep working, and rows with escaped Cyrillic
are read as before and become readable the next time they are saved.
| kenepa/translation-manager | this plugin |
|---|---|
Kenepa\TranslationManager\TranslationManagerPlugin::make() |
Asignua\FilamentTranslationManager\TranslationManagerPlugin::make() |
config available_locales (code, name, flag) |
same shape, also ->locales() / ->availableLocales(); the flag is a country code |
config language_switcher |
same key (default false), or ->languageSwitcher() |
gate use-translation-manager |
manageTranslations; to keep the old one: ->gate('use-translation-manager') |
| "Synchronize" / publish translations | Scan code action and translation-manager:scan (plus --prune, --dry-run) |
spatie migration (vendor:publish --provider=Spatie\TranslationLoader...) |
not needed; ours skips an existing table |
composer remove kenepa/translation-manager && composer require asignua/filament-translation-manager- Swap the plugin class in your panel provider; keep your locales.
- Gate: rename the ability or call
->gate('use-translation-manager'). AdddeleteTranslations(or delete stays disabled). php artisan vendor:publish --tag=filament-translation-manager-migrations && php artisan migrate(adds thecreated_by_scancolumn).- Remove the old config file if you published it. This plugin reads
config/filament-translation-manager.php.
This list follows kenepa's README; check it against the version you run. Anything kenepa had that is not here (for example its own languages seeding) is intentionally left out.
#Gotchas
- Closed by default. No gate, no access, and no error: the navigation entry just is not there. Define
manageTranslations. - Always write through the model. Spatie caches every group and language forever; only the model's
saved/deletedevents flush it. A rawDB::table('language_lines')->update(...)leaves stale text on the site until the cache is cleared. - Dots are fine,
::is not.__('Mr. Smith')is a JSON-style key and works. Namespaced keys (pkg::group.key) are served by the package that owns them and cannot be overridden from the database loader. - A key that looks like
group.keyis treated as a group row only when a lang filegroup.phpexists (or the group already has rows); otherwise it is a JSON-style key with the whole string. - Dynamic keys are invisible to the scan. Add them by hand or keep them in lang files, and review before
--prune. - Stored text is limited to 255-character keys (the unique index is
group+key). - Two scanners must never disagree. The plugin has one literal parser for everything (
Support\TranslationLiteral), tested with escaped quotes and apostrophes inside double-quoted strings; do not add a second regex. - Already published spatie's migration? Fine. Ours skips an existing table. If you publish spatie's after ours it fails with "table exists": delete that file.
#Translations
The interface ships in English, Ukrainian, German, Spanish, French, Italian, Dutch, Polish, Brazilian Portuguese and Turkish
under the filament-translation-manager::filament-translation-manager namespace. A test keeps every language in step with the English keys. Override a
string by publishing the translations (--tag=filament-translation-manager-translations) and editing the copy in
lang/vendor/filament-translation-manager.
#AI agents
The package ships Laravel Boost guidelines (resources/boost/guidelines/core.blade.php) so a
coding agent wires it up correctly.
#Testing
composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pint --test
Try it by hand with the workbench:
vendor/bin/testbench workbench:build
vendor/bin/testbench filament:assets
vendor/bin/testbench db:seed --class="Workbench\Database\Seeders\DemoSeeder"
vendor/bin/testbench serve --host=0.0.0.0
Log in at /admin as [email protected] / password.
#Changelog
See CHANGELOG.md.
#More plugins by asign
- Translatable Fields — locale tabs for spatie/laravel-translatable forms
- SEO Files — sitemap, robots.txt and llms.txt
- Themes — per-user and per-panel colour themes
- Rich Editor Toolkit — paste cleanup, attributes and video links in the rich editor
All plugins: https://filamentphp.com/plugins?q=asign
#License
The MIT License (MIT). See LICENSE.md.
The author
asign is a small web-dev company from Lviv, Ukraine. We build business applications on Laravel and Filament — CRMs, automation systems for standard and non-standard business processes, booking and content management systems, including our own Filament-based CMS. We open-source the parts that prove useful beyond a single project
From the same author
Kanban
Drag-and-drop Kanban board with transition modals, filters and policies — the Filament 5 successor of mokhosh/filament-kanban.
Author:
asign
VAT ID
Tax ID and VAT number validation for Filament: offline format and checksum rules for EU VAT, PL NIP/REGON, CZ IČO/DIČ and UA ЄДРПОУ/РНОКПП, plus an input that looks companies up in VIES, ARES, the Polish white list and GUS.
Author:
asign
XLSX Export
Download the current table as a real Excel file: filters, search and sort applied, typed numbers and dates, streamed instantly. No queue, no CSV.
Author:
asign
Rich Editor Toolkit
Ten free RichEditor plugins: Word/Docs paste cleanup, image alt, captions and alignment, link rel options, fullscreen, HTML source view, and length limits.
Author:
asign
Featured Plugins
A selection of plugins curated by the Filament team
Custom Dashboards
Let your users build and share their own dashboards with a drag-and-drop interface. Define your data sources in PHP and let them do the rest.
Filament
Custom Fields
Eliminate custom field migrations forever. Let your users create and manage form fields directly in Filament admin panels with 20+ built-in field types, validation, and zero database changes.
Relaticle
Soft Theme
A theme that gives panels a warm, approachable look with rounded shapes, gentle colors, and serif headings.
Filament