rivalex/lingua
Lingua is a self-hosted translation manager for Laravel 11–13 that stores i18n strings in your database or lang files. It adds a Livewire admin UI plus CSV, JSON, XLSX, and ODS import/export with dry-run previews, RTL support, and 26 bundled locales.
Install and bootstrap in three commands:
composer require rivalex/lingua
php artisan lingua:install
php artisan config:clear
The installer is interactive and asks for a storage driver. Choose database for day-to-day work — it gives you instant edits without redeploys and is where the Livewire UI shines. It publishes config/lingua.php, runs migrations, and seeds the default locale with the bundled 26-locale dataset. Set LINGUA_STORAGE_DRIVER=database in .env and clear config.
Then log in (routes are under ['web','auth'] by default) and open your-app.test/lingua/languages. The first useful workflow: add a second locale, open /lingua/statistics to see coverage, then go to /lingua/translations/{locale} and fill gaps. That's the loop you'll repeat every sprint.
If you'd rather keep lang/ as the source of truth (typical for a small team that PRs language files), pick file mode instead — the installer writes PHP/JSON files and skips the language_lines table.
Database-first editing, file-export for review. The strongest day-to-day pattern is: translators edit in the UI (DB driver), then you export JSON/CSV from /lingua/transfer (always via the enforced dry-run preview) and commit the artefact to a PR for review. lingua:uninstall also exports before tearing down, which is a useful escape hatch if you ever migrate away.
Bi-directional sync is the bridge. Push local PHP/JSON lang files into the DB (or pull DB back to files) when you need to reconcile a release branch with what product edited in the UI. Treat one direction as authoritative per environment — usually pull DB → files locally and push files → DB in a fresh environment's seed step.
Bulk and machine translation. For new locales, bulk AI-translate via Lingua Pro (suppress_pro_nudge and pro_upgrade_url are the only config surface — everything else lives in the add-on). Route missing-key drilling from the statistics page into the translations editor for spot fixes.
Programmatic access. Use the Lingua facade in seeders, custom Artisan commands, and tests to read/write translations and manage languages without going through Livewire. This is the right seam for test fixtures — seed known translations in a base test case and assert against them.
Selector in the host app. Pick a selector.mode in config/lingua.php: sidebar, modal, dropdown, or headless (zero-CSS semantic HTML — the right choice if you have a design system). Flags toggle independently. For RTL locales nothing extra is needed; it's first-class.
Routing and layout. Override routes_prefix, add routes_extra_parameters for multi-tenant scoping (e.g. {team?}), and set layout when you want Lingua pages to render inside your app shell rather than the default livewire.layout. Set navigate: true only if your host app has Livewire SPA navigation enabled.
Authorization. Wire a Gate and set LINGUA_GATE=manage-translations. Middleware defaults to ['web','auth'], and when gate is set, can:{gate} is automatically appended to admin routes — don't duplicate it.
Vendor translations. Lingua manages published vendor lang files alongside your own, so you can patch a broken package string without forking. Keep vendor translation edits in a note or PR comment — they'll be overwritten on the next vendor sync.
UI niceties that pay off. ui.sticky_top (int → rem, or CSS string) fixes the filter bar under a fixed host header. Rich text (headings, bold, code-block, etc.) is configurable per project and also overridable from the Settings UI — keep the config as the baseline so dev environments match.
(locale, group). Translations are cached indefinitely with prefix lingua.trans. After seeding, bulk-importing, or editing programmatically, clear the relevant cache or you'll debug a phantom. LINGUA_CACHE_STORE lets you isolate this from the app's cache.LINGUA_STORAGE_DRIVER between database and file doesn't sync — the two stores are independent. Run the sync first if you want a snapshot. Also config:clear after switching; the driver is read from env.language_lines migration. If you toggle to database later, run php artisan migrate again — the table won't exist.gate with an undefined Gate = 403 for everyone, including you. Define the ability before setting LINGUA_GATE, or you'll lock out the UI in staging.layout: null delegates to livewire.layout. If the host app doesn't define that, full-page Lingua renders will fail. Set an explicit layout in any non-standard Laravel skeleton.LINGUA_FORCE_DARK_MODE=true overrides the OS/host theme entirely — fine for embedding, bad for a user who explicitly picked light. Prefer the class strategy (<html class="dark">) and let the host own the toggle.openspout/openspout. CSV/JSON work without it. Install it before telling translators to use spreadsheets.translation_loaders, model, and translation_manager are swappable via config. Override the model if you need soft deletes or extra columns; override LinguaManager only if you need to change driver behaviour — that's the deepest hook and the least stable.session_variable defaults to locale — if your app already uses that key for something else, rename it before the two fight.How can I help you explore Laravel packages today?