PHPackages                             syriable/laravel-localizer - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. syriable/laravel-localizer

ActiveLibrary[Templating &amp; Views](/categories/templating)

syriable/laravel-localizer
==========================

A modern, focused localization extraction engine for Laravel 13. Discovers, extracts, and normalizes translatable strings from Blade, PHP, Vue, JavaScript, TypeScript, Livewire, and Inertia source files.

v1.0.5(2mo ago)05MITPHP ^8.4

Since May 23Compare

[ Source](https://github.com/syriable/laravel-localizer)[ Packagist](https://packagist.org/packages/syriable/laravel-localizer)[ Docs](https://github.com/syriable/laravel-localizer)[ RSS](/packages/syriable-laravel-localizer/feed)WikiDiscussions Synced 3w ago

READMEChangelog (6)Dependencies (14)Versions (11)Used By (0)

    ![Syriable Localizer Logo](art/localizer-light.svg)

[![Latest Version on Packagist](https://camo.githubusercontent.com/30cf89c97ca7d820fce92ed2716ef3ffe0678a69aee252dbf079c2cc6c573a5c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7379726961626c652f6c61726176656c2d6c6f63616c697a65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/syriable/laravel-localizer)[![GitHub Tests Action Status](https://camo.githubusercontent.com/59f16a63368c17ff1ecdc6a2306d0b0b67a2e0ee6d4f3b1550fda7d84629649c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7379726961626c652f6c61726176656c2d6c6f63616c697a65722f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-localizer/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/a138241e9627965491863946ae972525e20b9d6c41ecea5da9c8a7e069f6a16f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7379726961626c652f6c61726176656c2d6c6f63616c697a65722f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-localizer/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![GitHub PHPStan Action Status](https://camo.githubusercontent.com/9b2588ac24003abd7f526056307b74d38a277ebe38ca82ecaed67a3b46d77598/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7379726961626c652f6c61726176656c2d6c6f63616c697a65722f7068707374616e2e796d6c3f6272616e63683d6d61696e266c6162656c3d7068707374616e267374796c653d666c61742d737175617265)](https://github.com/syriable/laravel-localizer/actions?query=workflow%3Aphpstan+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/608622fa73692a9b521f53127045bb6f534bbc47ce43356125d6b553dbbc067f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7379726961626c652f6c61726176656c2d6c6f63616c697a65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/syriable/laravel-localizer)

**Syriable Localizer** is a focused, modern extraction engine for Laravel 13. It discovers translatable strings in your Blade, PHP, Vue, JavaScript, TypeScript, Livewire and Inertia source files, normalizes them, and hands them back as typed immutable DTOs through a stable, contracts-driven API.

It is deliberately scoped to a single responsibility: **extraction**. It does not write language files, manage translators, or sync with third parties — those concerns belong to ecosystem packages built on top of this engine.

```
use Syriable\Localizer\Facades\Localizer;

$result = Localizer::scan();

foreach ($result->unique() as $string) {
    echo "[{$string->kind->value}] {$string->value}\n";
    echo "  at {$string->location->path}:{$string->location->line}\n";
}
```

Where this fits
---------------

[](#where-this-fits)

This package is the **extraction layer**: it gives you a clean, typed list of every translatable string in your codebase, with source locations and stable fingerprints. What you do with that list is up to you.

Common consumers of a `ScanResult`:

- **Generate language files.** For each short key, `$string->langFilePath($locale)` returns the full on-disk path (e.g. `lang/en/profile/buttons.php` or `lang/vendor/syriable/en/profile/buttons.php`); `$string->key` is the dotted path inside the file. For JSON keys, the same method returns `lang/{locale}.json`.
- **Detect missing translations.** Diff the unique strings against your existing language files to find untranslated keys.
- **Sync with translation vendors.** Push the unique strings to Lokalise, Crowdin, POEditor, or a custom translation backend; pull back the translations.
- **Audit translation coverage in CI.** Fail the build if scanned strings outnumber translated ones by more than X%.
- **Source-locate a key.** Given a key string, jump to `$location->path:$location->line` to find where it's used.

None of these are built into this package — they are downstream concerns that build on top of the engine. A companion writer/sync package is on the roadmap; in the meantime, the three [events](#extending-the-engine) plus the [`Localizer::normalize()`](#extending-the-engine) extension point give you everything needed to build your own.

Support us
----------

[](#support-us)

If this package helps you ship better Laravel applications, please consider [starring the repository](https://github.com/syriable/laravel-localizer) and following [@syriable](https://github.com/syriable) for updates on future ecosystem releases.

We highly appreciate hearing about how you're using it — open a Discussion to share, request a feature, or propose an extractor for a new source language.

Installation
------------

[](#installation)

Install via Composer:

```
composer require syriable/laravel-localizer --dev
```

**Requirements:** PHP 8.4+, Laravel 13.x.

The service provider is auto-discovered. Publish the config file with:

```
php artisan vendor:publish --tag="localizer-config"
```

This is the contents of the published config file:

```
return [

    'paths' => [
        resource_path('views'),
        resource_path('js'),
        app_path(),
    ],

    'exclude' => [
        '**/node_modules/**',
        '**/vendor/**',
        '**/storage/**',
        '**/bootstrap/cache/**',
        '**/.git/**',
        '**/tests/**',
    ],

    'extractors' => [
        'blade'      => \Syriable\Localizer\Extractors\BladeExtractor::class,
        'livewire'   => \Syriable\Localizer\Extractors\LivewireExtractor::class,
        'inertia'    => \Syriable\Localizer\Extractors\InertiaExtractor::class,
        'php'        => \Syriable\Localizer\Extractors\PhpExtractor::class,
        'vue'        => \Syriable\Localizer\Extractors\VueExtractor::class,
        'typescript' => \Syriable\Localizer\Extractors\TypeScriptExtractor::class,
        'javascript' => \Syriable\Localizer\Extractors\JavaScriptExtractor::class,
    ],

    'cache' => [
        'enabled' => true,
        'path'    => storage_path('app/.localizer/cache.json'),
    ],

    'lock' => [
        'name'    => 'localizer:scan',
        'seconds' => 60,
    ],

];
```

**Extractor registration order matters.** When two extractors match the same file (e.g. `*.blade.php` matches both Blade and PHP), the first registered wins. The default order is correct for typical Laravel applications.

Usage
-----

[](#usage)

### A simple scan

[](#a-simple-scan)

```
use Syriable\Localizer\Facades\Localizer;

$result = Localizer::scan();
```

### An explicit request

[](#an-explicit-request)

```
use Syriable\Localizer\Data\ScanRequest;

$result = Localizer::scan(new ScanRequest(
    paths: [resource_path('views'), resource_path('js')],
    exclude: ['**/node_modules/**', '**/legacy/**'],
    extractors: ['blade', 'vue'],
    useCache: true,
));
```

### The fluent builder

[](#the-fluent-builder)

```
$result = Localizer::in([resource_path('views'), resource_path('js')])
    ->exclude('**/legacy/**')
    ->only(['blade', 'vue'])
    ->fresh()
    ->scan();
```

The builder is immutable — every method returns a new instance, so configurations can be safely shared and reused.

### Working with the result

[](#working-with-the-result)

```
$result->strings;                  // list, in discovery order
$result->count();                  // total strings including duplicates
$result->unique();                 // list deduplicated by fingerprint
$result->groupedByKind();          // ['short_key' => [...], 'json_key' => [...]]
$result->groupedByFile();          // ['pagination' => [...], 'buttons' => [...]]

$result->filesScanned;             // total files (cached + fresh)
$result->filesFromCache;           // files served from cache
$result->filesFresh();             // files extracted this run
$result->durationMs;               // float

$result->toArray();                // serializable representation
```

### The `ExtractedString` DTO

[](#the-extractedstring-dto)

For ShortKeys, the value is decomposed into the filesystem target plus the key inside the target file:

```
$string->value;         // 'Welcome back', 'pagination.next', or 'syriable::profile/buttons.submit.label'
$string->kind;          // StringKind::JsonKey | StringKind::ShortKey
$string->extractor;     // 'blade'

// ShortKey decomposition (all null/empty for JsonKey):
$string->package;       // 'syriable' (vendor namespace before `::`), or null
$string->directories;   // ['profile'] or ['profile', 'button', 'form'] — directory chain
$string->file;          // 'buttons' (the PHP file, without `.php`)
$string->key;           // 'submit.label' (the dotted path inside the file)

$string->location;      // SourceLocation { path, line, column }
$string->filePath();    // 'profile/buttons.php' — relative file path, or null for JsonKey
$string->langFilePath('en'); // 'lang/en/profile/buttons.php' or 'lang/vendor/syriable/en/...' — full on-disk path
$string->fingerprint(); // xxh128 content hash for deduplication
$string->toArray();
```

All DTOs are PHP 8.4 `readonly` — immutable by language guarantee, not by convention.

### Short keys vs JSON keys

[](#short-keys-vs-json-keys)

Every extracted string is classified by **how it maps onto Laravel's translation file layout**, not by where it was extracted from. The classification determines which file the translation lives in and how the key resolves inside it.

**ShortKey** — a value that points at a PHP file and a key inside it. The general grammar:

```
[package::]dir1/dir2/.../file.key.subkey

```

with `package`, every `dir`, `file`, and every key segment being lowercase alphanumeric with `_` or `-`. Examples:

ValuepackagedirectoriesfilekeyDestination file`pagination.next``null``[]``pagination``next``lang/{locale}/pagination.php``auth.failed.attempts``null``[]``auth``failed.attempts``lang/{locale}/auth.php``profile/buttons.submit.label``null``['profile']``buttons``submit.label``lang/{locale}/profile/buttons.php``profile/button/form/icon.submit.label``null``['profile', 'button', 'form']``icon``submit.label``lang/{locale}/profile/button/form/icon.php``syriable::buttons.submit``syriable``[]``buttons``submit``lang/vendor/syriable/{locale}/buttons.php``syriable::profile/buttons.submit.label``syriable``['profile']``buttons``submit.label``lang/vendor/syriable/{locale}/profile/buttons.php`The key path resolves inside the file's returned array via `data_get($array, $key)`. For `submit.label` against `['submit' => ['label' => 'Save']]`, the value is `'Save'`.

A string is classified as ShortKey only when it strictly matches the grammar above. Any whitespace, mixed case, punctuation, or malformed structure (`/buttons.submit`, `profile//x.y`, `::buttons.x`) falls through to **JsonKey** — in which case the entire string IS the key and the translation lives in `lang/{locale}.json`:

ValueDestination file`Welcome back``lang/{locale}.json``You have :count messages``lang/{locale}.json``Click here to continue``lang/{locale}.json`The name "JsonKey" refers to Laravel's terminology for translations whose **keys are stored in a JSON file** — it has nothing to do with whether the string was extracted from a `.json` source file (the package doesn't scan those). All JsonKeys go into the single `lang/{locale}.json` file.

### The Artisan command

[](#the-artisan-command)

A single command. Every option is explicit and predictable.

```
# Use config defaults
php artisan localizer:scan

# Specific paths
php artisan localizer:scan resources/views resources/js

# Ignore the cache
php artisan localizer:scan --fresh

# Limit to specific extractors
php artisan localizer:scan --extractor=blade --extractor=vue

# JSON output (for piping into other tools)
php artisan localizer:scan --json

# Summary only
php artisan localizer:scan --summary
```

The command always exits with `0` on a successful scan — an empty result is not an error.

### Caching and incremental scans

[](#caching-and-incremental-scans)

Every scan computes an [xxh128](https://github.com/Cyan4973/xxHash) content fingerprint for each discovered file. The cache (a single JSON document at `storage/app/.localizer/cache.json`) maps each file path to its last-known fingerprint and the strings extracted at that time. On the next scan:

- File fingerprint matches → strings loaded from cache (no re-read, no re-extraction).
- File fingerprint differs → file re-extracted, cache updated.

A no-change scan over thousands of files completes in tens of milliseconds. The cache is written atomically (`rename()` over a temporary file) and protected by Laravel's lock provider so parallel CI runs do not corrupt each other.

**Cache versioning.** The cache file embeds a schema version. When you upgrade between major versions of this package, the format may change incompatibly and the old cache will be rejected. If you see a `LocalizerException` mentioning a cache-version mismatch after upgrading, run the scan once with `--fresh` to rebuild it:

```
php artisan localizer:scan --fresh
```

This is a one-time cost per major upgrade. Minor and patch versions preserve cache compatibility.

### Extending the engine

[](#extending-the-engine)

The engine has three extension points. All of them follow standard Laravel conventions — no engine subclassing required.

**1. Custom extractors.** Implement `Syriable\Localizer\Contracts\Extractor` and register the class in `config('localizer.extractors')`:

```
use Syriable\Localizer\Contracts\Extractor;
use Syriable\Localizer\Data\DiscoveredFile;

final class TwigExtractor implements Extractor
{
    public function name(): string
    {
        return 'twig';
    }

    public function patterns(): array
    {
        return ['*.twig'];
    }

    public function extract(DiscoveredFile $file, string $contents): iterable
    {
        // yield ExtractedString instances...
    }
}
```

**2. Normalizers.** Transform or drop strings before they reach the result. A normalizer is any callable or `Normalizer` implementation that returns one of three values:

- **The original `ExtractedString`** → the string passes through unchanged.
- **A new `ExtractedString`** → the original is replaced. Use this to rewrite values, change kinds, attach groups, etc.
- **`null`** → the string is dropped from the result entirely.

Normalizers run in registration order and apply to every scan. They must be pure functions of their input — no I/O, no shared mutable state.

```
// Drop strings: filter out debug keys.
Localizer::normalize(fn ($string) => str_starts_with($string->value, 'debug.') ? null : $string);

// Transform strings: trim whitespace from values.
Localizer::normalize(fn ($string) => $string->withValue(trim($string->value)));

// Filter + transform in one normalizer: drop empties, lowercase the rest.
Localizer::normalize(function ($string) {
    $trimmed = trim($string->value);

    return $trimmed === '' ? null : $string->withValue(strtolower($trimmed));
});
```

Clear all registered normalizers with `Localizer::withoutNormalizers()` — useful in tests or when temporarily disabling a downstream package's normalizers.

**3. Events.** For everything else — telemetry, progress bars, downstream packages — listen to one of the three events:

EventFiredPayload`ScanStarted`Before discovery`ScanRequest``FileExtracted`Once per file, including cache hits`DiscoveredFile`, `list`, `bool $fromCache``ScanCompleted`After normalization, before return`ScanResult````
use Syriable\Localizer\Events\ScanCompleted;
use Illuminate\Support\Facades\Event;

Event::listen(ScanCompleted::class, function (ScanCompleted $event) {
    info("Scan complete: {$event->result->count()} strings");
});
```

Analyzing translation calls
---------------------------

[](#analyzing-translation-calls)

Beyond simple key extraction, the package can perform a deeper inspection of every translation call site — capturing not just the key, but the **replacements array** and the kind of PHP expression behind each placeholder.

```
php artisan localizer:analyze                       # configured paths
php artisan localizer:analyze app/ resources/views  # explicit paths
php artisan localizer:analyze --json                # structured output
php artisan localizer:analyze --key=auth.failed     # filter by key
```

For every `__()`, `trans()`, `@lang()`, `trans_choice()`, `Lang::get()`or `Lang::choice()` call the analyzer extracts the literal key plus every `:placeholder => $expression` pair, then classifies each expression into one of eight typed categories: `variable`, `object_property`, `nested_object_property`, `function_call`, `method_call`, `static_method_call`, `literal`, `expression`.

Example: for `__('welcome', ['name' => $user->profile->full_name, 'count' => getOrdersCount($user->id)])` the JSON output contains:

```
{
    "key": "welcome",
    "function": "__",
    "placeholders": [
        {
            "placeholder": ":name",
            "source": "$user->profile->full_name",
            "type": "nested_object_property",
            "structure": { "object": "$user", "path": ["profile", "full_name"] }
        },
        {
            "placeholder": ":count",
            "source": "getOrdersCount($user->id)",
            "type": "function_call",
            "structure": { "function": "getOrdersCount", "arguments": ["$user->id"] }
        }
    ],
    "lang_example": { "welcome": "Welcome :name :count" }
}
```

This is the engine behind the AI generation strategy's prompt-enrichment step, and it is exposed standalone so downstream tooling (linters, audits, IDE plugins) can reason about placeholder shapes.

Generating translation files
----------------------------

[](#generating-translation-files)

The package ships a companion `localizer:generate` command that turns a scan result into actual PHP translation files. It's an optional convenience built on top of the extraction engine — the engine itself remains write-free.

```
php artisan localizer:generate                 # uses app.locale
php artisan localizer:generate --locale=fr
php artisan localizer:generate --all-locales   # from localizer.generator.locales
php artisan localizer:generate --dry-run       # preview without writing
php artisan localizer:generate --strategy=key  # humanized | key | empty | ai
php artisan localizer:generate --namespace=acme
php artisan localizer:generate --force         # overwrite existing values
php artisan localizer:generate --fresh         # ignore scan cache
php artisan localizer:generate --no-analyze    # skip call-site analysis
```

Before generating, the command automatically runs the call-site analyzer over your source paths and passes the resulting placeholder context to any strategy that implements `AnalysisAwareStrategy` (most notably the `ai` strategy). Use `--no-analyze` to disable this step.

For every scanned ShortKey, the generator writes the missing keys to the corresponding PHP file under `lang/{locale}/` (or `lang/vendor/{package}/{locale}/` for vendor-namespaced keys), preserving the directory structure encoded in the key. JsonKey strings are written to `lang/{locale}.json`. A key like `profile/btn/form.submit.label`produces:

```
// lang/en/profile/btn/form.php
