Skip to content
Devix Open Source

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

Updated 15 Sep 2026