syriable/laravel-localizer
Syriable Localizer is a modern extraction engine for Laravel 13 that scans Blade, PHP, Vue, JS/TS, Livewire and Inertia files to discover and normalize translatable strings, returning typed immutable DTOs via a stable, contracts-driven API.
Full Changelog: https://github.com/syriable/laravel-localizer/compare/v1.0.4...v1.0.5
Full Changelog: https://github.com/syriable/laravel-localizer/compare/v1.0.3...v1.0.4
Full Changelog: https://github.com/syriable/laravel-localizer/compare/v1.0.2...v1.0.3
Full Changelog: https://github.com/syriable/laravel-localizer/compare/v1.0.1...v1.0.2
Full Changelog: https://github.com/syriable/laravel-localizer/compare/v1.0.0...v1.0.1
Full Changelog: https://github.com/syriable/laravel-localizer/commits/v1.0.0
First stable release. The public API — Localizer, PendingScan, every
Data\* DTO, every Contracts\* interface, the localizer:scan command,
the localizer.* config keys, and the on-disk cache schema — is now
covered by semver. Patch releases are bug fixes only; minor releases add
backwards-compatible features; major releases may break the API.
ExtractedString. ShortKeys now
expose package, directories, file, and key — a four-field
decomposition that mirrors Laravel's on-disk language-file layout.
filePath() returns the relative path to the target PHP file and
langFilePath($locale) returns the full on-disk path including the
lang/ prefix and the vendor/{package}/ segment when applicable.
Locales are validated against a safe character set to prevent
path-traversal injection.ScanResult::groupedByFilePath() keys ShortKey buckets on the full
relative path (e.g. profile/buttons.php), avoiding the directory
collision that the basename-keyed groupedByFile() exhibits.ScanCache::prune(array $knownPaths) evicts cache entries for files
that no longer exist on disk, keeping the cache file from growing
unbounded. PersistCache invokes it automatically at the end of each
scan.LocalizerException::lockTimeout(int $seconds, Throwable $cause)
wraps Laravel's LockTimeoutException so callers only need to catch
the package's own exception type.StringClassifier exposes packageFor(), directoriesFor(),
fileFor(), and keyFor() for downstream consumers that want to
decompose raw key strings without constructing an ExtractedString.LivewireExtractor matches *Component.php in addition to
*Livewire*.php, covering the common Livewire 3 suffix convention.ScanResult: skippedExtensions counts files
discovered but skipped because no extractor matched, surfaced in both
the summary output and --json output of the Artisan command.group and namespace fields on
ExtractedString were replaced by the four-field tree decomposition
(package, directories, file, key). Downstream code that wrote
language files must update — see the migration notes below.ScanResult::groupedByShortKeyGroup()
renamed to groupedByFile(). Behaviour is unchanged.--fresh is required.ExtractedString::fingerprint() now incorporates the full
decomposition (package, directories, file, value), so values
targeting different translation files are never deduplicated together.ScanCommand table output replaces the "Group" column with separate
"Package" and "File" columns.DiscoveredFile::$relativePath is always forward-slash separated,
regardless of host OS. Fixes a Windows-specific test failure and
ensures exclusion globs behave identically on every platform.CallExtractor no longer matches method calls like this.t('x') or
router.t('y') as translation invocations. The negative lookbehind
now excludes . in addition to identifier characters.InertiaExtractor::FUNCTIONS no longer lists
usePage().props.translations, which is a property accessor and not
a callable.ScanPipeline dispatches ScanStarted after acquiring the lock
(not before), so listeners receive the event at the moment the scan
actually begins — not up to lockSeconds earlier.ScanPipeline catches Illuminate\Contracts\Cache\LockTimeoutException
and re-throws as LocalizerException::lockTimeout() so callers only
need to import the package's own exception namespace.LocalizerException::lockTimeout message now references the correct
config key (localizer.lock.seconds, not localizer.lock_seconds).DiscoverFiles::relativise() is hardened with a separator-boundary
check so overlapping scan roots (/foo/bar and /foo/barbaz) cannot
produce an incorrect relative path.StringClassifier::fileFor() and keyFor() defensively guard
against undefined regex capture indices — required for PHPStan level 8
cleanliness.CallExtractor caches compiled regex patterns per function-name list,
amortising the preg_quote + implode cost across all files in a
scan to a single build per extractor.Syriable\Localizer\Contracts\ResultStore
interface was unused by the engine and is removed. Downstream
consumers that wrote against it should reach for the
Events\ScanCompleted event or the ScanResult returned by
Localizer::scan() instead.If you adopted the 0.9.x beta, the upgrade requires two code-level changes:
// 0.9.0 — group + namespace
$file = $string->group ?? '';
$namespace = $string->namespace; // could be 'syriable::profile' (compound)
// 1.0.0 — four-field tree decomposition
$file = $string->file;
$package = $string->package; // just the vendor: 'syriable'
$directories = $string->directories; // ['profile']
$key = $string->key; // 'submit.label'
For language-file path generation:
// 0.9.0 — string-parse the namespace to build the path
$path = $string->namespace
? "lang/vendor/{str_replace('::', '/', $string->namespace)}/{$locale}/{$string->group}.php"
: "lang/{$locale}/{$string->group}.php";
// 1.0.0 — call the helper
$path = $string->langFilePath($locale);
If you implemented Syriable\Localizer\Contracts\ResultStore, switch to
listening for the Events\ScanCompleted event:
use Syriable\Localizer\Events\ScanCompleted;
use Illuminate\Support\Facades\Event;
Event::listen(ScanCompleted::class, fn (ScanCompleted $e) => $store->put($e->result));
First public beta release.
Extractor, Discoverer, Normalizer, ScanCache.ScanRequest, ScanResult, ExtractedString, DiscoveredFile, SourceLocation, StringKind.storage/app/.localizer/cache.json — outside Laravel's
cache:clear blast radius, so incremental scans survive cache flushes.ScanStarted, FileExtracted, ScanCompleted.localizer:scan Artisan command with --fresh, --extractor, --json, --summary flags.Localizer::in(...) builder.Localizer::scan(ScanRequest) API.ScanResult::$skippedExtensions — files discovered but with no matching extractor are counted by extension, surfaced via the result DTO, displayed by localizer:scan summary, and included in --json output.LocalizerException::forFile(DiscoveredFile, \Throwable) — wraps per-file extractor failures with file context, preserving the original cause via getPrevious().LocalizerException::cacheVersionMismatch(int $found, int $expected) — typed exception with --fresh remediation hint.run-tests, phpstan, fix-php-code-style-issues, update-changelog.phpstan-baseline.neon) committed at empty state.SECURITY.md with GitHub Security Advisory channel.How can I help you explore Laravel packages today?