zeerats / laravel-lang-lint
Artisan commands that lint Laravel language files: missing keys, unused keys and consistent formatting.
Requires
- php: ^8.3
- illuminate/collections: ^12.0||^13.0
- illuminate/console: ^12.0||^13.0
- illuminate/contracts: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
- symfony/finder: ^7.2||^8.0
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/pint: ^1.29
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.6||^5.0
- pestphp/pest-plugin-type-coverage: ^4.0||^5.0
- phpstan/extension-installer: ^1.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Artisan commands that keep PHP language files in sync with the code that uses them.
| Command | What it does | Exit status |
|---|---|---|
lang:check |
Reports keys used in the source code that a language file does not define. | 1 when keys are missing |
lang:unused |
Reports keys in the language files that no code uses. --prune removes them. |
1 when keys are unused and not pruned |
lang:format |
Rewrites language files with sorted keys in one consistent style. --check only verifies. |
1 when --check finds unformatted files |
The commands never prompt. Files change only when you pass --prune or run lang:format without --check, so the commands are safe in git hooks, CI pipelines and any other non-interactive shell.
Invalid options or configuration, such as a locale without a directory, produce a one-line error and exit status 2.
Requirements
- PHP 8.3 or newer
- Laravel 12 or 13
Installation
composer require --dev zeerats/laravel-lang-lint
Publish the configuration file when the defaults do not fit your project:
php artisan vendor:publish --tag=lang-lint-config
Usage
Missing translations
php artisan lang:check php artisan lang:check --show-locations php artisan lang:check --locale=de --path=app/Http
+--------------------------+------------+
| Key | Missing in |
+--------------------------+------------+
| messages.only_in_en | de |
| projects.archive_confirm | de, fr |
+--------------------------+------------+
ERROR Found 2 translation keys missing from at least one locale.
A key is missing when at least one locale does not define it. --show-locations adds a column with the files and lines that use each key.
Unused translations
php artisan lang:unused php artisan lang:unused --prune
Without --prune the command lists the unused keys with the locales that define them and exits with status 1. With --prune it removes the keys and rewrites the affected files in the canonical style described below. Parents that a removal leaves empty are removed as well.
Formatting
php artisan lang:format php artisan lang:format --check php artisan lang:format --prune
lang:format rewrites every language file of the selected locales in one canonical style:
- keys are sorted recursively by byte value, lists keep their order
- four spaces of indentation, single-quoted strings and a trailing comma after every entry
- a blank line after
return [and before];, matching the language files Laravel ships - comments are dropped, because the file is rendered from its data
--check reports the files that would change and exits with status 1, which makes it a formatting gate for CI. --prune removes unused keys in the same pass.
Options shared by all commands
| Option | Effect |
|---|---|
--path=app --path=routes |
Scan these directories instead of the configured paths. Relative to the base path. |
--locale=en --locale=de |
Work on these locales instead of every discovered one. |
Configuration
config/lang-lint.php:
return [ // Directories scanned for translation calls, relative to the base path. 'paths' => ['app', 'resources/views'], // Functions, static calls and Blade directives whose first argument is a key. 'functions' => ['__', 'trans', 'trans_choice', '@lang', '@choice', 'Lang::get', 'Lang::has', 'Lang::choice'], // Locales to check. Empty: every directory in the language path except vendor. 'locales' => [], // Regular expressions for locale directories to skip during discovery. 'ignore_locales' => [], // Regular expressions for keys that count as used without a literal call. 'assume_used' => ['/^auth\./', '/^pagination\./', '/^passwords\./', '/^validation\./'], ];
assume_used covers two situations: keys the framework reads itself, such as validation messages, and keys your code builds at runtime, such as __("statuses.{$status}"). Both are never reported as unused and never pruned. Keys that appear literally in the code are still checked for missing translations.
How keys are detected
- Every
.phpfile below the configured paths is scanned, so Blade templates are included. - A call counts when its first argument is a single string literal, such as
__('messages.welcome'), also when the call spans several lines. - Interpolated keys such as
__("messages.{$type}")and concatenated keys such as__('messages.' . $type)cannot be resolved statically and are skipped. Add a pattern toassume_usedso their targets are kept. - Namespaced keys such as
package::messages.welcomebelong to packages and are skipped. - Strings without a dot or with whitespace are JSON translation strings and are skipped. The package checks PHP language files only.
- A key used as a prefix marks everything below it as used:
__('messages.types')returns the whole array, somessages.types.imagecounts as used. - Detection is textual. A call inside a comment counts as a usage, and a call made through a variable function name does not.
Continuous integration
GitLab CI:
translations: script: - php artisan lang:check - php artisan lang:format --check
GitHub Actions:
- run: php artisan lang:check - run: php artisan lang:format --check
A pre-commit hook can run the same two commands.
Development
composer test
This runs PHPStan, Pint, Pest's type coverage and the test suite.
Changelog
See CHANGELOG.md.
License
MIT, see LICENSE.md.