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

Linguist Laravel Package

keevitaja/linguist

keevitaja/linguist is a Laravel localization package that stores translations in the database instead of flat files, letting you edit them through a web UI, auto-detect missing keys, and manage multi-language content without touching language files.

View on GitHub
Deep Wiki
Context7

Getting Started

Linguist is a database-backed translation layer for Laravel: instead of hand-editing resources/lang/*.php, you write the source string inline in your code, and Linguist records it as a translatable "text" with per-locale translations that you fill in through a small web UI at /linguist.

Minimal setup (Laravel 5.x era):

composer require keevitaja/linguist

Register the provider and facade in config/app.php (this predates package auto-discovery):

Keevitaja\Linguist\LinguistServiceProvider::class,

'Linguist' => Keevitaja\Linguist\Facades\Linguist::class,

Publish the config and migrations, then migrate:

php artisan vendor:publish --provider="Keevitaja\Linguist\LinguistServiceProvider"
php artisan migrate

Where to look first:

  1. config/linguist.php — the locales array and default locale are the only knobs that matter on day one.
  2. The published migrations — they define the schema (texts + translations table). You'll be querying these when something renders in the wrong language.
  3. src/LinguistServiceProvider.php — confirm how the /linguist route is registered and what middleware guards it, because you will want to change it.

First use case: take one hard-coded string in a Blade view:

<h1>{{ ling('Welcome back') }}</h1>

Load the page (the string gets registered), then visit /linguist in your browser, add et/fr/whatever as a locale, and type the translation next to the source text. Set the active locale the usual Laravel way (App::setLocale($locale) or a middleware reading the session) and reload — the translated string renders, and any locale you haven't filled in falls back to the source text.

That's the whole loop. Everything else is scaling it.

Implementation Patterns

Inline text keys, not dotted keys. Linguist keys off the source string itself, so the idiomatic style is ling('Sign in to your account') rather than __('auth.signin'). This is great for marketing copy and bad for anything you'll want to reword later (see Gotchas). Keep a convention: use ling() for application copy, keep __() for framework strings (validation, pagination, auth) so you don't end up with two half-populated systems.

Placeholders. String replacement follows the usual :name convention:

ling('Hello, :name', ['name' => $user->first_name]);

Pass only scalar replacements, and be consistent about placeholder names across locales or the translation will silently render with a literal :name.

Locale selection belongs in your app, not the package. Linguist does not negotiate locales for you. The standard pattern is a SetLocale middleware that reads session('locale'), validates it against config('linguist.locales'), and calls App::setLocale(). Pair it with a tiny switcher route:

Route::get('lang/{locale}', function ($locale) {
    abort_unless(in_array($locale, config('linguist.locales')), 404);
    session(['locale' => $locale]);
    return back();
});

Register the middleware in the web group so every request resolves a locale before views render.

Protect the translation UI. The /linguist screen is a write endpoint — anyone who reaches it can rewrite your copy. Wrap it in auth immediately, either by editing the route group in the service provider or by adding a global middleware that blocks /linguist* unless the user is an admin. Treat it as an internal tool, not a feature.

Seed translations for CI and fresh installs. Because translations live in the database, a fresh migrate gives you an empty translation table and every page renders in English. Write a seeder that inserts the texts and translations you already have (or dump production rows with mysqldump --where), and run it in your deploy pipeline. Otherwise every new developer and every CI run sees untranslated output.

Cache aggressively. Every ling() call that misses the in-memory map is a database round trip. For a page with 40 strings that's 40 queries. The pattern that works: a singleton repository that loads the full translation map for the current locale once per request (Cache::remember("linguist.{$locale}", 3600, ...)) and flushes it whenever the UI saves a translation. If you can't touch the package, wrap ling() in your own helper that reads from a cached array and only calls through on a miss.

Bulk onboarding. If you're retrofitting Linguist onto an existing app, script the conversion: grep your views and lang files for user-facing strings, generate ling() calls, and insert the extracted pairs into the translations table with a one-off artisan command. Doing this by hand through the web UI does not scale past a few dozen strings.

Escape properly. Print translations with {{ }}. Translations are user-supplied data from the admin UI; treating them as trusted HTML with {!! !!} is an XSS vector the moment a non-developer has UI access.

Gotchas and Tips

It's archived — budget for a fork. No commits since the Laravel 5.x era, zero dependents, and the composer.json constraint won't satisfy modern Laravel. You will be patching it: bump the framework constraint, replace anything using removed helpers (str_*, array_*), and check that the service provider still registers routes the way current Laravel expects. Decide this before you build on it, not after.

Source text is the key, which makes typos permanent. Fix "Recieve" to "Receive" in your code and every translation for that string is orphaned — the old row sits there, the new string appears untranslated in every locale. Keep a rule: never edit a source string once it's translated, and if you must, write a migration that renames the key in the texts table alongside the code change.

Identical strings collapse. Two different ling('Save') calls — one for a form button, one for a settings panel — share one translation row. If you need context-specific wording in another language, you have to differentiate the source (ling('Save changes') vs ling('Save')) or you can't.

Missing translations fail silently. The fallback is the source text, which is exactly right in production but hides gaps in review. Add a check to your test suite: iterate config('linguist.locales'), assert a non-empty translation exists for a known list of critical strings, and fail CI on gaps.

Migration name collisions. The published migrations create fixed table names. If you have anything called texts or translations already, rename in the migration before running it — and do that on a scratch database first, since a half-run migration set is annoying to unpick.

config:cache and the locale list. If you add a locale to config/linguist.php in production and forget to re-run php artisan config:cache, the new locale won't appear anywhere — not in the UI, not in your switcher's validation. Same trap as any package config, but here the symptom (a locale silently missing) looks like a bug in Linguist.

Debugging "why is this in English": check in this order — (1) App::getLocale() at render time, (2) does a row exist in the texts table for that exact string including punctuation and capitalization, (3) does a translation row exist joining that text to the locale, (4) is your cache stale. Ninety percent of reports are step 2, caused by a trailing space or a smart quote.

Extension points are thin. The package exposes a config file, a facade, and a Blade-facing helper — that's about it. If you need per-domain grouping, pluralization rules, or a translation memory, you're forking. Since it's archived anyway, vendor the source into your app (app/Support/Linguist) and own it; that's cheaper than fighting a dev-master constraint in CI.

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