misbar/laravel-lang-scanner

Scan Blade templates, extract translatable strings, generate language files, optionally replace hardcoded text with __() helpers, and machine-translate into other locales.

Maintainers

Package info

github.com/devwsafi/misbar

Homepage

Issues

pkg:composer/misbar/laravel-lang-scanner

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-08-03 21:04 UTC

This package is auto-updated.

Last update: 2026-08-03 21:38:51 UTC


README

Tests Latest Version License

Scan Blade templates, extract hardcoded user-visible strings, generate/update lang/{locale}/{group}.php (or .json) files, optionally replace the hardcoded text with __() translation helpers, and optionally machine-translate the extracted strings into another locale.

php artisan lang:scan

Requirements

  • PHP 8.2+ (targets PHP 8.4)
  • Laravel 11 / 12 / 13

Installation

composer require misbar/laravel-lang-scanner

The package auto-registers itself via Laravel's package discovery — no manual provider registration needed. If you've disabled discovery, add it yourself in bootstrap/providers.php (Laravel 11+) or config/app.php (Laravel 10 and below):

Misbar\LangScanner\Providers\LangScanServiceProvider::class,

Publish the config (optional):

php artisan vendor:publish --tag=langscan-config

Other publishable tags:

Tag Publishes to
langscan-config config/langscan.php
langscan-views resources/views/vendor/langscan/
langscan-assets public/assets/js/custom/

Usage

# Scan the whole project (resources/views + Modules/*/resources/views)
php artisan lang:scan

# Scan a directory
php artisan lang:scan resources/views/orders

# Scan a single file
php artisan lang:scan resources/views/orders/create.blade.php

# Scan a Laravel Modules package
php artisan lang:scan Modules/Orders/resources/views

# Replace hardcoded strings in-place with __() helpers
php artisan lang:scan --replace --backup

# Auto-translate into Arabic using the configured provider
php artisan lang:scan --translate --locale=ar --source=en

# Preview everything without writing any files
php artisan lang:scan --dry-run -v

# Generate JSON language files instead of PHP arrays
php artisan lang:scan --json

# Use a custom group/file name
php artisan lang:scan --group=orders

# Rebuild language files from scratch (ignores existing translations)
php artisan lang:scan --force

Options

Option Description
--replace Replace hardcoded strings with {{ __('group.Key') }} helper calls
--translate Machine-translate extracted strings into --locale
--force Rebuild language files from scratch instead of merging
--dry-run Preview only, no files are written
--locale=ar Target locale for --translate
--source=en Source locale for the base generated language file
--group=x Translation file/group name (defaults to messages)
--json Generate lang/{locale}.json instead of lang/{locale}/{group}.php
--backup Backup each Blade file (.bak-YYYYMMDD-HHMMSS) before --replace
-v Show detailed per-file logs (standard Laravel verbosity flag)

Translation providers

Configured via config/langscan.php -> translator.driver:

  • mymemory (default, free, no API key required)
  • google (LANGSCAN_GOOGLE_API_KEY)
  • deepl (LANGSCAN_DEEPL_API_KEY)
  • openai (LANGSCAN_OPENAI_API_KEY, LANGSCAN_OPENAI_MODEL)

Swapping providers is a one-line .env change; no code changes required (Strategy pattern via Misbar\LangScanner\Contracts\TranslatorInterface).

What gets extracted

  • Text inside whitelisted tags: h1-h6, p, span, label, button, a, li, option, td, th, caption, legend, dt, dd, small, strong, b, i, em, summary, div, title
  • Whitelisted attributes: placeholder, title, alt, aria-label, content, label, value, data-title, data-label, data-placeholder

What is always ignored

  • {{ }}, {!! !!}, {{-- --}} and Blade/HTML comments
  • @if/@foreach/@switch/... directive expressions (their wrapped HTML is still scanned normally)
  • @php ... @endphp, <script>, <style> blocks
  • <x-component> / <x-slot> tags
  • wire:*, :*, @*, v-* bindings
  • Numeric-only or punctuation-only strings
  • Any string containing dynamic (non-static) content

Architecture

src/                                       Namespace root: Misbar\LangScanner\
├── Console/Commands/LangScanCommand.php   Thin orchestrator (the artisan command)
├── Contracts/                             ScannerInterface, ParserInterface, TranslatorInterface
├── DTOs/                                  TranslationItem, ScanResult, ScanStatistics, RunOptions, RunResult
├── Http/Controllers/                      LangScanController - thin HTTP adapter around LangScanRunner
├── Services/
│   ├── BladeScanner.php                   Walks files, delegates parsing, de-duplicates
│   ├── BladeParser.php                    Thin adapter: file content -> TranslationItem[]
│   ├── BladeReplacer.php                  Safely rewrites Blade files in-place
│   ├── LangFileGenerator.php              Merges/writes lang/{locale}/{group}.php|.json
│   ├── LangScanRunner.php                 Shared orchestrator used by both CLI and Web UI
│   ├── TranslationService.php             Batches + gracefully degrades translation calls
│   ├── ReportGenerator.php                Console report rendering
│   └── Translators/                       Strategy implementations (Google/DeepL/MyMemory/OpenAI)
├── Support/
│   ├── FileCollector.php                  Lazy, memory-efficient file discovery
│   ├── StringCleaner.php                  Normalises + validates candidate strings
│   ├── HtmlStringExtractor.php            Core masking + regex extraction engine
│   └── BladeHelpers.php                   Shared whitelist/regex constants
└── Providers/LangScanServiceProvider.php  Registers command, config, views, routes, TranslatorInterface binding

resources/views/langscan/index.blade.php   Web UI screen (registered under the `langscan::` view namespace)
resources/langscan-assets/js/custom/       Web UI frontend JS
routes/langscan.php                        Web UI routes (only loaded when web_ui.enabled)
config/langscan.php                        Package configuration

Extraction strategy

HtmlStringExtractor builds a length-preserving "masked" copy of the file content, where every ignored region (echoes, directives, comments, scripts, components, dynamic bindings) is overwritten with a sentinel byte character-for-character. Because the masked copy is exactly the same length as the original, every match offset found against it is valid against the original file too - which is what allows BladeReplacer to perform precise, surgical substr_replace operations that never disturb surrounding markup, indentation, or unrelated code.

This regex/offset-based approach was chosen over a full HTML+Blade AST parse specifically for performance on large codebases (thousands of Blade files), per the project's optimization requirements.

Web UI (Metronic v8)

A browser-based screen is included at /lang-scan: browse your resources/views tree, select one or more files/folders, configure options, and run the scan without touching the terminal. Results (extracted strings, source file/line, and the language file(s) they were/will be saved to) render on the same page.

Setup

The Web UI is registered automatically by the package — no manual file copying required after composer require.

  1. Publish the frontend asset (Metronic-styled vanilla JS):

    php artisan vendor:publish --tag=langscan-assets

    This copies langscan.js to public/assets/js/custom/langscan.js. Adjust the <script> path in the view if your public asset layout differs (publish --tag=langscan-views first if you need to edit the Blade file).

  2. In config/langscan.php -> web_ui.layout, set the name of your actual Metronic v8 master layout (defaults to layouts.master). The view uses @extends($layout ?? 'layouts.master'), @section('content'), and the styles/scripts stacks — if your layout doesn't support @push/@stack, publish the view with --tag=langscan-views and adjust it directly.

  3. By default the routes are gated behind ['web', 'auth'] middleware (config('langscan.web_ui.middleware')) — this tool can rewrite source files and read your whole views tree, so keep it behind auth (or disable entirely with LANGSCAN_WEB_UI_ENABLED=false) outside local development.

  4. Visit /lang-scan (or your configured web_ui.route_prefix).

How it works

  • The folder tree lazy-loads: expanding a directory calls POST /lang-scan/browse which lists only that directory's immediate children (fast even on huge trees, and directory traversal outside the project root is rejected server-side).
  • Checking a file OR a folder adds its relative path to the selection; folders are scanned recursively server-side (same FileCollector used by the CLI).
  • "Run Scan" posts the selection + options to POST /lang-scan/run, which runs through Misbar\LangScanner\Services\LangScanRunner - the exact same orchestration code path used by php artisan lang:scan - and returns JSON: statistics, every extracted string with its source file/line/group, and the language file(s) written (or that would be written, in dry-run mode) with added/total key counts.
  • --dry-run is checked by default in the UI so a first run is always a safe preview before anyone opts into writing files.

Testing

composer install
composer test
# or target a specific suite:
vendor/bin/phpunit --testsuite=Unit
vendor/bin/phpunit --testsuite=Feature   # requires orchestra/testbench

All Support/Service classes are covered by isolated unit tests with zero Laravel bootstrapping required; Feature tests boot a minimal app via Orchestra Testbench to exercise the full lang:scan command end-to-end. CI runs the full matrix (PHP 8.2-8.4 × Laravel 11-13) on every push and PR — see .github/workflows/tests.yml.

Known limitations

  • Regex-based extraction (not a full AST parser) may not correctly handle deeply nested same-named HTML tags (e.g. <div><div>...) inside a single whitelisted tag body.
  • Translation keys containing a literal . character can collide with Laravel's "group.item" dot notation for nested array keys; review generated keys with periods before relying on them in production.
  • The value attribute is extracted generically; consider excluding it via a custom BladeHelpers::TEXT_ATTRIBUTES override if your project uses value="" heavily for non-UI-text purposes (e.g. hidden inputs).

Contributing

Issues and pull requests are welcome at github.com/misbar/laravel-lang-scanner. Please run composer test before opening a PR.

License

The MIT License (MIT). See LICENSE for details.