amirami/localizator
Localizator is a Laravel package for managing localization files. It scans your application for translation strings, generates and updates language files, and provides a web interface for editing translations. Simplify multilingual Laravel projects with automatic scanning and convenient translation...
Install (dev-only). Localizator is a build-time tool, not a runtime dependency. Keep it out of production:
composer require --dev amirami/localizator
php artisan vendor:publish --tag=localizator
The publish tag may be localizator or localizator-config depending on version — if the tag fails, run php artisan vendor:publish and pick the package from the interactive list.
Read the config first. Everything the tool does is driven by config/localizator.php. Before running anything, open it and confirm these four knobs:
| Key | What it controls | Day-one advice |
|---|---|---|
target_locales |
Which locale files get written | Start with just ['en'] |
search_paths |
Directories scanned | Confirm app/, resources/views/, routes/ are covered; add packages/ or modules if you use a monorepo layout |
exclude / exclude_files |
Paths and filenames skipped | Always include vendor, node_modules, storage, bootstrap/cache |
patterns |
Regexes used to find translatable strings | Defaults cover __(), trans(), @lang, trans_choice; extend for your own helpers |
First run — on a branch, with a clean tree.
git checkout -b chore/extract-translations
git status # must be clean so the diff is reviewable
php artisan localize
The command scans the configured paths, prints what it extracted, and writes/updates the locale files (JSON or PHP depending on config). Then git diff — that diff is the deliverable. Review it before committing; it is your first and best signal for false positives.
The first use case to target. The highest-value scenario is a project that was built with English literals inline — {{ __('Welcome back') }} scattered across 200 Blade files. Point Localizator at it, generate a single canonical lang/en.json, commit it, and you now have a translation backlog you can hand to a translator or pipe into a machine-translation step. Do not start by adding five locales; get one locale file correct and stable first, then fan out.
The healthy loop separates three concerns that Localizator deliberately does not combine:
php artisan localize regenerates the key set from source.git diff to reject comment matches, dynamic-key false positives, and duplicates.Run extraction on a cadence (pre-release, or weekly in CI as a report), not on every save. Treat the generated file as a reviewed artifact, not a build output.
The single most consequential config decision. Message strings that contain dots ("Please confirm the file name.") collapse into nested arrays when written to PHP files, which then can't be looked up as a flat key. Rule of thumb:
auth.php, validation.php, passwords.php structure.Mixing both in one project works, but decide per-domain and write it down, because a string that lands in the wrong format fails silently at runtime (__('some.key') returns the key itself).
patterns for your own conventionsOut of the box you get the framework's helpers. Most teams have one or two wrappers. Add them rather than rewriting call sites:
'patterns' => [
// ... defaults ...
'/\bt\(\s*[\'"](.+?)[\'"]\s*[,)]/',
'/@translatable\(\s*[\'"](.+?)[\'"]\s*\)/',
],
Also point search_paths at anything the defaults miss: Livewire component classes, Blade components under resources/views/components, route files with ->name(__('...')), or an Inertia/Vue directory if you want to regex JS.
The cheapest integration that pays for itself: fail the build when source has translatable strings that aren't in the locale file.
- name: Check translation coverage
run: |
php artisan localize
git diff --exit-code lang/ || (echo "New untranslated strings found. Run: php artisan localize" && exit 1)
This forces developers to commit extracted strings in the same PR that introduces them. It's coarse (it will also fail on ordering churn — see sort_keys below), but it stops the "we'll extract before release" debt from accumulating.
If sort_keys is available in your version, enable it on the first run, before the file grows. Turning it on later rewrites the entire file in one commit and poisons git blame for every key. A stable sort order also makes the CI diff check above reliable — without it, map-ordering noise causes spurious failures.
Don't try to extract a 50k-line project in one shot. Scope search_paths to a module at a time:
'search_paths' => [
base_path('app/Modules/Billing'),
],
Localize billing, ship it, then add the next module. Each run only writes to its locale files, so the diffs stay reviewable and the translator batches stay small.
Localizator's output is plain Laravel lang files — the exact input format for laravel-lang/* community translations and for most TMS imports (Crowdin, Lokalise, Phrase). Keep Localizator as the source of keys and let the TMS own the values; don't let both write to lang/.
It is regex, not an AST. This is the defining limitation and explains most surprises. Expect blind spots:
__('Hello ' . $name) — the pattern won't resolve the full string.__($message), __("errors.{$code}") — correctly skipped, but they are untranslated strings that will never appear in the diff. Audit these manually.It adds keys; it generally doesn't curate them. Verify on a branch whether your version overwrites existing values or only appends missing keys — behaviour here matters enormously if you have hand-edited translations. Assume it will not remove stale keys either: when a string is deleted from source, its translation lingers. Periodic pruning is a manual job.
lang/ moved in Laravel 9+. Pre-9 projects use resources/lang/, newer ones use lang/ at the project root. If the command reports writing files but nothing changes, your lang_path in config is pointing at the wrong place. Check the package's Laravel version constraint against your framework version too.
Clean git tree, always. There's no reliable dry-run flag across versions. The git diff after a run is your only review surface. Running with uncommitted work means you can't tell extracted strings apart from your own edits.
Exclude generated and vendored code aggressively. Compiled Blade views land in storage/framework/views and will be re-scanned on every run — noisy, slow, and full of duplicated strings. Same for bootstrap/cache, vendor, node_modules, and any published package views you don't own.
Dotted keys collide and nest. In PHP lang files, __('auth.failed') is a nested lookup. If you also have a UI string that happens to be "auth.failed", one clobbers the other. JSON files dodge this entirely; that's the reason to prefer them for prose.
Performance scales poorly on huge trees. The scanner is a plain recursive file walk plus per-file regex. On a repo with tens of thousands of PHP/Blade files, expect minutes. Restrict search_paths rather than trying to tune it.
Pin the version and treat upgrades as review-worthy. NOASSERTION licensing means you should confirm the terms with your legal/procurement process before committing the dependency manifest — an unlabelled license is not the same as MIT.
It doesn't validate your translations. A key present in en.json but missing in de.json is invisible to this tool. Pair it with a coverage assertion (array_diff_key between locale files) if missing translations are a real production risk.
Extension points are narrow by design. Realistically you have two: the patterns regexes and the search_paths/exclude scoping. There's no plugin API for custom extraction logic. If you need AST-accurate extraction (for concatenations or dynamic keys), you're outside this package's scope and should look at a PHP-parser-based approach instead — accept that and use Localizator for the 85% it handles well.
How can I help you explore Laravel packages today?