Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Inline Translation Laravel Package

beyondcode/laravel-inline-translation

BeyondCode's Laravel Inline Translation lets you edit your app's translations directly in the browser—click text, translate inline, and save without leaving the page. It streamlines localization for Laravel projects and keeps translation files up to date.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the baseline first. This package's last release is July 2018 and it targets Laravel 5.6–5.8 / PHP 7.1–7.2. Before you spend time on anything else, run a timeboxed spike on a branch: composer require beyondcode/laravel-inline-translation (you'll likely need --ignore-platform-reqs on PHP 8, and a config.platform.php pin or replace entry so composer update doesn't churn). Boot the app and hit a page. If the service provider doesn't register cleanly, stop and decide between a vendored fork and a ~1–2 day reimplementation — do not sink time into making Composer resolve a dead dependency.

Where to look first (in this order):

  1. src/ — the whole package is readable in an afternoon: the service provider, one Blade directive, one middleware, a Translation model, and a migration. That's the entire surface.
  2. The published config (config/inline-translation.php) — this is where the enable flag, locale, and access gating live. Read it before you register anything.
  3. The migration — despite the marketing copy claiming "zero database changes," there is a translations table. Resolve this contradiction immediately: it determines whether your source of truth is resources/lang/*.json or the DB.
  4. The middleware — it rewrites HTML responses on the way out. Understand exactly what it touches before it's in the web group.

Minimum wiring:

php artisan vendor:publish --provider="BeyondCode\InlineTranslation\InlineTranslationServiceProvider"
php artisan migrate

Register the middleware in app/Http/Kernel.php — in the web group, last — and only behind your auth middleware:

protected $middlewareGroups = [
    'web' => [
        // ... EncryptCookies, StartSession, ShareErrorsFromSession, ...
        \BeyondCode\InlineTranslation\Http\Middleware\InlineTranslationMiddleware::class,
    ],
];

Then wrap copy in your Blade views with the package's directive (check src/ for the exact name in your version) instead of {{ __('...') }}:

<h1>@inlineTrans('marketing.hero.title')</h1>
<p>@inlineTrans('marketing.hero.subtitle')</p>

First use case to validate: a single marketing/landing page on local or staging, editor authenticated, one locale. Load the page, edit a string in the browser, confirm where it persisted (DB row vs. file write). That one loop tells you whether the package's data model matches what your team needs, and it's the whole decision in miniature.

Seed before you translate. Point the editor at an empty table and you'll retranslate from zero. Write a one-off command that reads resources/lang/*.json (and lang/en/*.php if you have PHP arrays) and inserts rows, so the inline editor opens on your existing copy.


Implementation Patterns

Wrap narrowly, not globally. The value is in-content editing by non-developers. Wrap marketing pages, email templates rendered to HTML, and admin-facing copy — not your entire resources/views. Every wrapped string is a translation key you now own, and a full-app wrap turns a 20-minute copy fix into a key-management project.

Two-tier gating (non-negotiable). Environment flag and authorization:

// config/inline-translation.php
'enabled' => env('INLINE_TRANSLATION_ENABLED', false),

// .env.staging
INLINE_TRANSLATION_ENABLED=true
// app/Http/Kernel.php — keep the editor behind auth
'web' => [
    // ...
    \App\Http\Middleware\Authenticate::class,
    \BeyondCode\InlineTranslation\Http\Middleware\InlineTranslationMiddleware::class,
],

Even better: wrap the package middleware in your own that checks App::environment() !== 'production' && $request->user()?->can('edit-translations'). Treat this as a security control, not a convenience — the middleware injects interactive affordances into HTML responses, and an unauthenticated editor endpoint is an attack surface.

Pick one canonical store and sync one direction. You have resources/lang/*.json and a translations table. Choose the DB as write target and files as deploy artifact:

// app/Console/Commands/ExportTranslations.php
foreach (Translation::all()->groupBy('locale') as $locale => $rows) {
    $payload = $rows->pluck('value', 'key')->sortKeys()->all();
    file_put_contents(
        resource_path("lang/{$locale}.json"),
        json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
    );
}

Run it on a schedule or as a pre-deploy step, commit the output, and never hand-edit the JSON afterward. Bidirectional sync is the pattern that produces silent, unexplainable overwrites.

Stable keys, decided up front. marketing.hero.title, not the English string. Keying on source copy orphans every translation the first time marketing changes a word — and that failure shows up weeks later, not at launch.

Locale workflow. Editing is per-locale and per-page. Make the editor's life explicit: switch locale via your normal locale switcher, edit the current page, export, deploy. Do not build a "translate everything" bulk screen on top of this — that's the TMS gap, and the package has no translation memory, glossary, or review state to support it.

Blade wrapper for consistency. If you wrap dozens of strings, a small component keeps keys consistent and makes CI scanning trivial:

{{-- resources/views/components/t.blade.php --}}
@inlineTrans($key)

CI guardrail. Add a test or artisan command that asserts every key referenced by the directive exists in the translations table for each supported locale, and fails the build on miss:

public function test_all_referenced_keys_exist(): void
{
    $keys = collect(File::allFiles(resource_path('views')))
        ->flatMap(fn ($f) => preg_match_all("/@inlineTrans\('([^']+)'\)/", $f->getContents(), $m) ? $m[1] : []);
    foreach (config('app.supported_locales') as $locale) {
        $missing = $keys->diff(Translation::where('locale', $locale)->pluck('key'));
        $this->assertEmpty($missing, "Missing {$locale}: ".$missing->implode(', '));
    }
}

Feature tests around rendered output. Because the middleware rewrites HTML, a simple test asserting a wrapped key renders text in the active locale catches both directive registration and middleware ordering regressions in one shot.

When it outgrows the pattern, reimplement rather than fork. The package is a directive + middleware + table + model — perhaps 200 lines. If inline editing is a durable requirement on modern Laravel, rebuilding that surface gives you a modern middleware signature, proper config() gating, and tests, usually cheaper than carrying a 2018 fork through the next two framework upgrades.


Gotchas and Tips

config:cache freezes the enable flag. Your INLINE_TRANSLATION_ENABLED gating is read at config-cache build time. Cache production config without the flag and staging config with it, and make it an explicit, reviewed deploy step — not an .env edit someone makes on a Friday.

Middleware ordering is fragile. Place it last in its group. Anything that rewrites the body — minifiers, laravel-responsecache, debug bars, ob_gzhandler, gzip/brotli middleware — will either strip the injected markup or corrupt it. Also expect it to be a no-op or a problem on StreamedResponse, BinaryFileResponse, and file downloads: the package expects a string body from a normal view response.

It only works on HTML rendered through Blade. JSON responses, API routes, Inertia/JS front ends, and anything rendered client-side get nothing from this. If the product is heading headless, the core mechanism — rewriting the outgoing HTML — doesn't apply, and no amount of config makes it.

Missing-key behavior shapes the UX. Confirm during the spike whether an untranslated key falls back to the key itself or the default locale string. Editors behave very differently when the page shows marketing.hero.title versus readable English. Don't assume; check.

Two sources of truth will bite. The DB row and the JSON file diverge the first time someone hand-edits lang/en.json after a DB export. Pick DB → files, commit the artifact, and add a CI check that the committed JSON matches what the exporter produces.

Blade directive collisions. If your app (or another package) already registers the same directive name, Blade::directive() silently loses one. Check Blade::getCustomDirectives() if the wrapper renders literally on the page; alias the directive in your provider if it collides.

Never enable in production. The middleware injects editing affordances and (depending on wiring) exposes a write path. It is a dev/staging tool. Add a defensive abort_unless(! app()->isProduction(), 404) inside your gate and treat a production flag as an incident, not a config rollback.

Escape editor output. Persist values as plain strings and render through {{ }} as usual; do not let edited copy flow into {!! !!}. Non-developer editors plus an HTML write path is an XSS vector.

Archived means "vendored source," not "dependency." Pin the exact commit, consider copying src/ into app/Support/InlineTranslation/ (or a path repository), and delete the Composer constraint. That way composer update can't pull a broken resolution, and PHP 8 deprecations show up in your static analysis rather than in an untouchable vendor tree.

Extension points are thin. There are no contracts to swap or events to hook beyond what you read in src/. Customization is "publish and edit" — which is exactly why you should decide between vendoring and reimplementing early, and why you should read the whole package before writing your first wrapped view.

Clear the caches when in doubt. If edits don't appear: php artisan view:clear && php artisan config:clear && php artisan route:clear. In 90% of "the package is broken" cases on this era of Laravel, it's a stale compiled view or a cached config flag.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin