· 6 min read

Find Hardcoded Text in Your Laravel App with Code Scanner

We've all been there: you're internationalizing a Laravel app and need to find every hardcoded string. You open the codebase and realize there are hundreds of files to check, and no grep pattern reliably separates "user-facing English sentence" from "CSS class" or "log message". The Code Scanner changes that. It finds translatable strings in your Blade and PHP files, suggests keys, and rewrites your source code to use __(). Here's the full workflow, including the diff it produces and the places where you still need to pay attention.

The Problem in a Real Laravel App

Here's a perfectly normal Blade view from an app that grew faster than its i18n discipline:

{{-- resources/views/home.blade.php --}}
<h1>Welcome to our app</h1>
<p>Manage your orders and invoices in one place.</p>

<form method="POST" action="{{ route('newsletter.subscribe') }}">
    @csrf
    <input type="email" name="email" placeholder="Enter your email">
    <button type="submit">Submit</button>
</form>

And the matching controller:

// app/Http/Controllers/AuthController.php
if (! Auth::attempt($credentials)) {
    throw new Exception('Invalid credentials');
}

Five user-facing strings across two files, and this app has two hundred files like them. Finding these by hand means reading every view, every exception message, every placeholder and alt attribute. It's the kind of task that stalls i18n projects for months, which is why so many "multilingual" Laravel apps still greet French users with English button labels.

The Walkthrough: Scan, Review, Apply

The scanner works in three phases. One important ground rule first: run these commands locally, not on production. The apply phase modifies your source files, and you want git diff and git checkout available to review and revert.

  1. Scan your codebase. From your project root:
php artisan translator:scan

The scanner analyzes .blade.php and .php files and detects HTML content in tags (headings, buttons, paragraphs), HTML attributes like placeholder, title, and alt, string literals in PHP code, and exception messages. You can narrow the sweep with --path=resources/views, skip folders with --exclude=tests, preview locally without sending anything with --dry-run, or filter noise with --min-confidence=50.

Every candidate gets a confidence score: 80-100% is a clear translation candidate, 50-79% deserves a look, and below 50% needs careful review. Known element types like headings and buttons, sensible text length (5-80 characters), and user-facing file locations all push confidence up. If you want the results somewhere other than your terminal, --format=json or --format=csv gives you output you can pipe into other tooling; the default --format=table is the readable one.

  1. Optionally, add --ai for smarter suggestions. Without AI, "You did it John!" becomes the literal key you-did-it-john. With --ai, it becomes congratulations with the value You did it, :name!; the AI generates semantic key names, detects dynamic content like names and dates and converts them into placeholders, and produces English keys even for non-English source text. This costs 1 quota unit per candidate, the same quota that powers AI translations, so a 300-candidate scan uses 300 units.

  2. Review in the web interface. Open Code Scanner in the LangSyncer sidebar and pick your project. Each suggestion shows the original text, the proposed key, a color-coded confidence badge, and every file location where the text appears. Approve, edit, or reject each one; the edit modal lets you change the key, tweak the value, or add placeholders, and offers both Save (keep it pending) and Save & Approve. For big scans, Approve All High Confidence handles the 80%+ items in one click, and you can filter by status, confidence, or file path to work through the rest.

  3. Apply the changes. Back in your terminal:

php artisan translator:apply

This rewrites your source files, activates the approved translations (draft to active), and regenerates the CDN. Run it with --dry-run first if you want to preview. The resulting diff on our example files:

 {{-- resources/views/home.blade.php --}}
-<h1>Welcome to our app</h1>
-<p>Manage your orders and invoices in one place.</p>
+<h1>{{ __('home.headings.welcome') }}</h1>
+<p>{{ __('home.text.manage_orders') }}</p>

 <form method="POST" action="{{ route('newsletter.subscribe') }}">
     @csrf
-    <input type="email" name="email" placeholder="Enter your email">
-    <button type="submit">Submit</button>
+    <input type="email" name="email" placeholder="{{ __('forms.placeholders.email') }}">
+    <button type="submit">{{ __('buttons.submit') }}</button>
 </form>
 // app/Http/Controllers/AuthController.php
 if (! Auth::attempt($credentials)) {
-    throw new Exception('Invalid credentials');
+    throw new Exception(__('errors.auth.invalid_credentials'));
 }
  1. Review the diff, test, commit. Run git diff, check for anything placed where PHP syntax doesn't allow a function call, run your test suite, then commit. From here, Translate Missing fills in every other language for the keys you just created.

Where the Scanner Has Limits

The scanner is a big accelerator, not a replacement for judgment. It uses heuristic analysis and is not 100% accurate about valid placement for __() calls: it can suggest translations in property declarations (public $placeholder = __('...') isn't valid PHP), constant definitions, or certain array key positions. That's exactly why the review phase exists and why you shouldn't blanket-approve medium and low confidence items. If a bad replacement slips through, git checkout -p reverts it surgically, and rejecting the corresponding review keeps it from coming back.

Scope is the other limit: only .blade.php and .php files are analyzed. JavaScript, TypeScript, and Vue components aren't currently supported, so if your frontend is a separate SPA, the scanner covers your Blade and backend strings but not your JS bundle. And if you're comparing i18n platforms specifically on in-context and frontend tooling, our LangSyncer vs Tolgee comparison covers where each approach fits.

For a codebase that's already 100% translated and enforced by CI conventions, you won't need the scanner often, but it stays useful as a regression check: run it after new features, before releases, or as part of code review to catch strings that snuck in hardcoded.

Gotchas

  • Placeholder variables are invented. When the scanner converts "Welcome, John!" into __('welcome.greeting', ['name' => $name]), the $name variable doesn't exist in your scope; your IDE will flag it as undefined, and you replace it with the real one, like $user->name.
  • Apply only processes approved reviews. If translator:apply seems to do nothing, check that your suggestions are approved rather than pending, and that your API key is configured.
  • Always --dry-run your first apply on a project, and keep a clean working tree so git diff shows only the scanner's changes.

Related Posts


Try the Code Scanner →

We use cookies to improve your experience and analyze site traffic. Cookie Policy

Cookie Preferences

Essential

Required for the site to work

Analytics

Help us improve the site

Marketing

Personalized ads and content