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
- PHP 8.2+ (targets PHP 8.4)
- Laravel 11 / 12 / 13
composer require misbar/laravel-lang-scannerThe 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-configOther publishable tags:
| Tag | Publishes to |
|---|---|
langscan-config |
config/langscan.php |
langscan-views |
resources/views/vendor/langscan/ |
langscan-assets |
public/assets/js/custom/ |
# 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| 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) |
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).
- 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
{{ }},{!! !!},{{-- --}}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>tagswire:*,:*,@*,v-*bindings- Numeric-only or punctuation-only strings
- Any string containing dynamic (non-static) content
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
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.
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.
The Web UI is registered automatically by the package — no manual file
copying required after composer require.
-
Publish the frontend asset (Metronic-styled vanilla JS):
php artisan vendor:publish --tag=langscan-assets
This copies
langscan.jstopublic/assets/js/custom/langscan.js. Adjust the<script>path in the view if your public asset layout differs (publish--tag=langscan-viewsfirst if you need to edit the Blade file). -
In
config/langscan.php->web_ui.layout, set the name of your actual Metronic v8 master layout (defaults tolayouts.master). The view uses@extends($layout ?? 'layouts.master'),@section('content'), and thestyles/scriptsstacks — if your layout doesn't support@push/@stack, publish the view with--tag=langscan-viewsand adjust it directly. -
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 withLANGSCAN_WEB_UI_ENABLED=false) outside local development. -
Visit
/lang-scan(or your configuredweb_ui.route_prefix).
- The folder tree lazy-loads: expanding a directory calls
POST /lang-scan/browsewhich 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
FileCollectorused by the CLI). - "Run Scan" posts the selection + options to
POST /lang-scan/run, which runs throughMisbar\LangScanner\Services\LangScanRunner- the exact same orchestration code path used byphp 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-runis checked by default in the UI so a first run is always a safe preview before anyone opts into writing files.
composer install
composer test
# or target a specific suite:
vendor/bin/phpunit --testsuite=Unit
vendor/bin/phpunit --testsuite=Feature # requires orchestra/testbenchAll 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.
- 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
valueattribute is extracted generically; consider excluding it via a customBladeHelpers::TEXT_ATTRIBUTESoverride if your project usesvalue=""heavily for non-UI-text purposes (e.g. hidden inputs).
Issues and pull requests are welcome at
github.com/misbar/laravel-lang-scanner.
Please run composer test before opening a PR.
The MIT License (MIT). See LICENSE for details.