Skip to content
Devix Open Source

Reference

The classes, without artisan

Everything the commands do is a plain class, so the most useful thing you can do with this package is put it in your own test suite.

Fail a test rather than a build

use Devix\Translations\{Catalogue, Linter, Scanner};

public function test_the_arabic_translations_are_not_broken(): void
{
    $used = (new Scanner)->scan([app_path(), resource_path()])->keys();

    $problems = (new Linter(
        Catalogue::load(lang_path(), 'en'),
        [Catalogue::load(lang_path(), 'ar'), Catalogue::load(lang_path(), 'ur')],
        $used,
    ))->run(['placeholder', 'plural', 'missing']);

    $this->assertSame([], array_map('strval', $problems));
}

array_map('strval', …) is worth doing: when it fails, PHPUnit prints the actual list of what is broken rather than "failed asserting that array is empty".

Catalogue

$en = Catalogue::load(lang_path(), 'en');     // both the PHP files and the JSON
$en->get('messages.nested.deep.key');
$en->has('messages.welcome');
$en->keys();
$en->sourceOf('messages.welcome');            // which file it came from

Catalogue::of('en', ['a.b' => 'Hello']);      // from an array, for a test

Catalogue::flatten(['a' => ['b' => 'x']]);    // ['a.b' => 'x']
Catalogue::expand(['a.b' => 'x']);            // ['a' => ['b' => 'x']]

Scanner

$scanner = (new Scanner)->scan([app_path(), resource_path(), base_path('routes')]);

$scanner->keys();      // ['messages.welcome' => [['file' => …, 'line' => 12]], …]
$scanner->dynamic();   // files where the key is a variable

It reads __, trans, trans_choice, @lang, @choice, Lang::get and $t() / $tc() in JavaScript — PHP, Blade, JS, TS and Vue.

Only literal keys are found, deliberately. __($key) cannot be resolved without running the program, and a scanner that guesses produces false positives people quickly learn to ignore. Those files are listed by dynamic() instead, so you know where to look before trusting unused.

PluralRules

PluralRules::forms('ar');       // 6
PluralRules::forms('ar-EG');    // 6
PluralRules::forms('ru');       // 3
PluralRules::forms('en');       // 2
PluralRules::forms('ja');       // 1

PluralRules::countIn('one|many');           // 2
PluralRules::usesRanges('{0} none|[1,*]');  // true

The counts come from Laravel's own MessageSelector::getPluralIndex, so the linter and the framework cannot disagree at runtime.

A string using explicit ranges — {0} none|[1,19] some|[20,*] many — is the author saying how many forms there are, so the plural check stands back.

Problem

$problem->reason;   // 'placeholder'
$problem->locale;   // 'ar'
$problem->key;      // 'messages.welcome'
$problem->detail;   // 'missing :name — it will render as literal text'
$problem->where();  // 'app/Http/Controllers/HomeController.php:34'
(string) $problem;  // '[placeholder] ar messages.welcome — missing :name …'

Porter

Porter::toCsv([$en, $ar]);            // the spreadsheet
Porter::fromCsv($csv);                // ['ar' => ['key' => 'value']]
Porter::write(lang_path(), 'ar', $lines);   // the PHP and JSON files
Updated 15 Sep 2026