Guide
Getting started
composer require devix-labs/laravel-translations --dev
php artisan translations:lint
That is the whole thing. No database table, no web UI, no migration. It reads the files you already have and exits non-zero when something is wrong, so it belongs in CI.
plural ..................................................... 1
ar messages.items ....... 2 plural forms where ar needs 6 — trans_choice will pick the wrong one
placeholder ................................................ 1
ar messages.welcome ..... missing :name — it will render as literal text
1 problem found.
The bug this exists for
Laravel's own MessageSelector returns six plural indices for Arabic:
$selector->getPluralIndex('ar', 0); // 0
$selector->getPluralIndex('ar', 1); // 1
$selector->getPluralIndex('ar', 2); // 2
$selector->getPluralIndex('ar', 3); // 3
$selector->getPluralIndex('ar', 11); // 4
$selector->getPluralIndex('ar', 100); // 5
So an Arabic trans_choice string needs six segments separated by |.
English needs two. A translator who has only ever seen English gives you two,
nothing complains, and in production trans_choice('messages.items', 3) returns
the wrong form — or the raw string with its pipes showing.
Nothing checks this. Not the existing translation managers, not Laravel.
The same class of bug one step down: :name is in the English string and gone
from the Arabic one, so the page says مرحبا where it should say مرحبا أحمد
and every test still passes.
Run against Laravel's own published Arabic pack, this finds two of them —
mimes and mimetypes both drop :attribute, so the user is told a file is the
wrong type without being told which field.
The six checks
missing |
Used in the code, not defined in this locale. |
unused |
Defined, never asked for. |
placeholder |
:name in the source string, absent from the translation. |
plural |
The wrong number of | forms for that locale's rules. |
empty |
Defined and blank. |
same |
Identical to the source, which usually means untranslated. |
php artisan translations:lint ar ur
php artisan translations:lint --only=placeholder,plural
php artisan translations:lint --except=unused,same
php artisan translations:lint --json
In CI
- name: Translations
run: php artisan translations:lint --except=unused
unused is usually the one to leave out of a build: a key reached through a
variable looks unused and is not.
Filling in what is missing
php artisan translations:sync ar ur # adds the keys, leaves them blank
php artisan translations:sync ar --copy # fills them with the English
php artisan translations:sync ar --dry # says what it would do
It never touches a value that is already there. New keys are added blank on
purpose, so translations:lint then reports them as empty and they cannot be
quietly forgotten.
Sending it to a translator
php artisan translations:export strings.csv ar --missing
# … the translator fills in the third column …
php artisan translations:import strings.csv
One column per locale, so the translator sees the English and their own language side by side rather than a list of keys with no context. The file carries a byte-order mark, which is the difference between Excel opening Arabic as Arabic and opening it as mojibake.
A blank cell means "not translated yet", never "erase what is there". An importer that writes empty cells over existing work destroys it; this one does not, and there is a test for that.
Where a value has changed on both sides, the file on disk wins unless you pass
--force, because a spreadsheet is usually the older copy.
Where to go next
- The commands — every option.
- The classes, without artisan — for your own tests.