rivalex / lingua
Laravel package for multilingual management with database, Livewire UI and translation synchronization
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- laravel-lang/common: ^6.7
- league/html-to-markdown: ^5.1
- livewire/flux: ^v2.12.0
- livewire/livewire: ^v4.1.0
- outhebox/blade-flags: ^1.6
- spatie/laravel-package-tools: ^1.92
- spatie/laravel-translation-loader: ^2.8
Requires (Dev)
- larastan/larastan: ^v3.0.0
- laravel/pint: ^v1.24.0
- nunomaduro/collision: ^v8.8.0
- orchestra/testbench: ^9.0 || ^10.9 || ^11.0
- pestphp/pest: ^v4.4.0
- pestphp/pest-plugin-arch: ^v4.0.0
- pestphp/pest-plugin-laravel: ^v4.0.0
- phpunit/phpunit: ^12.5.0
- spatie/laravel-ray: ^1.35.0
README
The complete multilingual management system for Laravel
Lingua brings database-driven translations to Laravel with a beautiful Livewire + Flux UI — install languages, manage translations, and sync everything with a single command.
Features · Installation · Configuration · Artisan Commands · Publishing · UI Guide · Facade · Architecture
Official Documentation
✨ Features
| Feature | Description |
|---|---|
| Database-backed translations | All translations stored in the database, editable instantly without deployments |
| Livewire UI | Reactive, real-time language and translation management interface |
| Flux UI components | Modern, accessible UI built with Livewire Flux |
| Bi-directional sync | Push translations to the database or pull them back to local PHP/JSON files |
| Laravel Lang integration | Install 70+ languages with one command, auto-updated via laravel-lang |
| Rich text support | Translations can be plain text, HTML, or Markdown |
| Language selector | Configurable sidebar, dropdown, or modal language switcher for users |
| Progress tracking | Per-language completion percentage and missing-translation counts |
| RTL support | First-class right-to-left language handling |
| Vendor translations | Manage package translations alongside your own |
| Database-agnostic | Full support for SQLite, MySQL, PostgreSQL, and SQL Server |
| Lingua Facade | Fluent programmatic API for reading, writing, and managing languages and translations |
| Fully tested | 150+ tests with Pest, covering commands, Livewire components, Blade components, helpers, and the Lingua facade |
📦 Requirements
- PHP 8.2+
- Laravel 11 | 12 | 13
- Livewire 4.0+
- Livewire Flux 2.0+
🚀 Installation
1. Install via Composer
composer require rivalex/lingua
2. Run the interactive installer
php artisan lingua:install
The installer will:
- Publish the configuration file to
config/lingua.php - Publish and run the database migrations
- Seed the database with your default language and its translations
- Optionally star the repo on GitHub ⭐
That's it — Lingua is ready.
3. Access the UI
| Page | URL | Route name |
|---|---|---|
| Languages | your-app.test/lingua/languages |
lingua.languages |
| Translations | your-app.test/lingua/translations/{locale?} |
lingua.translations |
⚙️ Configuration
After installation, config/lingua.php gives you full control:
return [ // Directory where local language files are stored 'lang_dir' => lang_path(), // Application default locale 'default_locale' => config('app.locale', 'en'), // Fallback locale when a translation is missing 'fallback_locale' => config('app.fallback_locale', 'en'), // Middleware applied to Lingua's routes 'middleware' => ['web'], // URL prefix for Lingua's management pages 'routes_prefix' => 'lingua', // Session key used to store the active locale 'session_variable' => 'locale', // Language selector widget settings 'selector' => [ 'mode' => 'sidebar', // 'sidebar' | 'modal' | 'dropdown' 'show_flags' => true, ], // Rich-text editor toolbar options 'editor' => [ 'headings' => false, 'bold' => true, 'italic' => true, 'underline' => true, 'strikethrough' => false, 'bullet' => true, 'ordered' => true, 'clear' => true, // ... more options available ], ];
🛠 Artisan Commands
Lingua ships with a complete command suite for terminal-driven language and translation management.
Language management
| Command | Description |
|---|---|
lingua:add {locale} |
Install a new language: downloads files, creates DB record, syncs translations |
lingua:remove {locale} |
Remove a language: deletes files, cleans DB, reorders remaining languages |
lingua:update-lang |
Update all installed language files via Laravel Lang, then re-sync to database |
# Add Italian php artisan lingua:add it # Add Brazilian Portuguese php artisan lingua:add pt_BR # Remove French (the default language is protected) php artisan lingua:remove fr # Pull the latest translation strings from Laravel Lang and sync to DB php artisan lingua:update-lang
Translation sync
| Command | Description |
|---|---|
lingua:sync-to-database |
Import all local PHP/JSON translation files into the database |
lingua:sync-to-local |
Export all database translations back to local PHP/JSON files |
# Populate the database from existing lang/ files (e.g. after a fresh install) php artisan lingua:sync-to-database # Write database translations to lang/ files (e.g. for version control or deployment) php artisan lingua:sync-to-local
Install command
# Interactive first-time setup wizard
php artisan lingua:install
📤 Publishing
Lingua ships several publishable groups so you can override only what you need.
Publish everything at once
php artisan vendor:publish --provider="Rivalex\Lingua\LinguaServiceProvider"
Publish individual tags
lingua-config
Publishes the configuration file to config/lingua.php.
php artisan vendor:publish --tag="lingua-config"
Use this when you want to customise routes, middleware, the language selector mode, the rich-text editor toolbar, or any other package option. The file is well-commented and safe to edit — Lingua reads it on every request.
lingua-migrations
Publishes the database migrations to database/migrations/.
php artisan vendor:publish --tag="lingua-migrations"
Use this when you need to modify the languages or language_lines table schema — for example to add indexes, change
column types, or integrate with an existing translations table. After publishing, run php artisan migrate as normal.
Note: The
lingua:installwizard publishes and runs the migrations automatically. Only publish manually if you need to customise the schema before running them.
lingua-translations
Publishes the package's own UI translation strings to lang/vendor/lingua/.
php artisan vendor:publish --tag="lingua-translations"
This exposes all the labels, headings, buttons, and messages used in the Lingua UI (e.g.
lingua::lingua.languages.title, lingua::lingua.translations.save). Override any string to translate the interface
into your application's language or to adapt the wording to your project's style.
The published files follow the standard Laravel vendor translation structure:
lang/
└── vendor/
└── lingua/
└── en/
└── lingua.php
lingua-views
Publishes all Blade and Livewire views to resources/views/vendor/lingua/.
php artisan vendor:publish --tag="lingua-views"
Use this to customise the look and layout of the Languages page, Translations page, individual translation rows, modals, or the language selector component. Laravel will use your published views instead of the package's defaults.
The full view tree is:
resources/views/vendor/lingua/
├── components/ # Blade anonymous components
│ ├── autocomplete.blade.php
│ ├── clipboard.blade.php
│ ├── editor.blade.php
│ ├── language-flag.blade.php
│ ├── menu-group.blade.php
│ └── message.blade.php
└── livewire/ # Livewire component views
├── languages.blade.php
├── language-selector.blade.php
├── translations.blade.php
└── translation/
├── create.blade.php
├── delete.blade.php
├── row.blade.php
└── update.blade.php
Tip: Only publish views you intend to change. Unpublished views are served directly from the package and will receive upstream updates automatically.
lingua-assets
Publishes the compiled CSS and JavaScript assets to public/vendor/lingua/.
php artisan vendor:publish --tag="lingua-assets"
This is required only if you serve assets from public/ rather than loading them via Vite or a CDN. Re-run this command
after every Lingua upgrade to keep the assets in sync with the package version.
Re-publishing after upgrades
After updating Lingua via Composer, re-publish any assets that may have changed:
# Force-overwrite previously published assets php artisan vendor:publish --tag="lingua-assets" --force php artisan vendor:publish --tag="lingua-translations" --force
The --force flag overwrites existing files. Omit it for views and config so your local customisations are not lost.
🖥 UI Guide
Languages page — /lingua/languages
The languages page is your control center for installed locales.
Available actions:
- Add a language — choose from 70+ locales; files are installed and translations synced automatically
- Remove a language — confirmation modal prevents accidental deletion; the default language is protected
- Set the default language — one click sets the new application default
- Reorder languages — drag-and-drop to control display order across the UI
- Sync to database — import all local
lang/files into the database - Sync to local — export database translations back to
lang/files - Update via Laravel Lang — pull the latest strings from upstream
laravel-langpackages
Each language row shows the completion percentage and a count of missing translations so you can prioritise your translation effort.
Translations page — /lingua/translations/{locale?}
Manage individual translation strings with a filterable, paginated table.
Available actions:
- Search by key, group, or value
- Filter by locale, group, or translation type (text / HTML / Markdown)
- Show only missing translations for a locale to focus your translation work
- Create new custom translation entries
- Edit any string inline — the rich-text editor activates automatically for HTML and Markdown types
- Delete translations globally or for a specific locale only
- Copy the translation key to clipboard with one click
RTL / LTR text direction
Some languages (Arabic, Hebrew, Persian, Urdu, …) are written right-to-left. Lingua stores the text direction for every
installed language and exposes it via Lingua::getDirection(). To support RTL layouts correctly you need to propagate
the direction to the root <html> tag so that the browser, CSS, and screen readers all behave correctly.
Add dir and lang attributes to the <html> element in your main Blade layout:
{{-- resources/views/layouts/app.blade.php --}} <!DOCTYPE html> <html lang="{{ app()->getLocale() }}" dir="{{ Lingua::getDirection() }}">
Lingua::getDirection() returns 'rtl' for right-to-left languages and 'ltr' for all others. It uses the current
application locale by default, so it automatically follows every locale switch without any extra code.
You can also pass an explicit locale when you need the direction outside of the current request context:
{{-- e.g. inside a per-language preview or email template --}} <html lang="ar" dir="{{ Lingua::getDirection('ar') }}">
Tailwind CSS
If your project uses Tailwind CSS, the dir attribute on <html> activates Tailwind's built-in rtl: variant
automatically — no additional configuration required:
<div class="text-left rtl:text-right">…</div> <div class="pl-4 rtl:pr-4 rtl:pl-0">…</div>
CSS logical properties (recommended)
For new layouts, prefer CSS logical properties over directional ones so the browser handles the flip for you:
/* Instead of: padding-left / padding-right */ padding-inline-start: 1 rem ; /* left in LTR, right in RTL */ padding-inline-end: 1 rem ; /* Instead of: border-left */ border-inline-start: 1 px solid ;
Checking direction in Blade
@if (Lingua::getDirection() === 'rtl') {{-- RTL-specific markup or classes --}} @endif
Note:
Lingua::getDirection()defaults to'ltr'if the locale is not found in the database, so it is always safe to call even before any language is installed.
Language selector component
Embed a language switcher anywhere in your Blade layouts:
<livewire:lingua::language-selector />
Control the display mode via config or inline props:
{{-- sidebar (default), dropdown, or modal --}} <livewire:lingua::language-selector mode="dropdown" :show-flags="false" />
Note: To show or hide the language flags, set the
lingua.show_flagsconfig option totrueorfalse. Alternatively, use the:show-flagsprop to override the config setting for a specific instance.
💎 Lingua Facade
Lingua ships a static Lingua facade that gives you programmatic access to language and translation data from anywhere
in your application.
Locale helpers
use Rivalex\Lingua\Facades\Lingua; // Current application locale (mirrors app()->getLocale()) Lingua::getLocale(); // 'en' // Locale marked as default in the database Lingua::getDefaultLocale(); // 'en' // Check whether a locale is the default Lingua::isDefaultLocale(); // true — uses current app locale Lingua::isDefaultLocale('fr'); // false // Check whether a locale is installed Lingua::hasLocale('fr'); // true / false // Change the default locale (persisted in the database) Lingua::setDefaultLocale('fr');
Language metadata
// English display name for a locale Lingua::getLocaleName(); // 'English' — uses current locale Lingua::getLocaleName('fr'); // 'French' // Native name Lingua::getLocaleNative(); // 'English' Lingua::getLocaleNative('ar'); // 'العربية' // Text direction Lingua::getDirection(); // 'ltr' Lingua::getDirection('ar'); // 'rtl'
Language collections
// All installed languages (Eloquent Collection) Lingua::languages(); // All languages with completion statistics // Each model includes: total_strings, translated_strings, missing_strings, completion_percentage Lingua::languagesWithStatistics();
Translation statistics
// Stats for a specific locale (or current locale if omitted) Lingua::getLocaleStats('fr'); // [ // 'total' => 1240, // 'translated' => 980, // 'missing' => 260, // 'percentage' => 79.03, // ]
Reading translations
// All locale variants for a key (returns ?array) Lingua::getTranslations('welcome'); // ['en' => 'Welcome', 'fr' => 'Bienvenue', 'de' => 'Willkommen'] // Single locale value (empty string if missing) Lingua::getTranslation('welcome'); // uses current locale Lingua::getTranslation('welcome', 'fr'); // 'Bienvenue' // All translations for a group, optionally filtered to a locale Lingua::getTranslationByGroup('validation'); Lingua::getTranslationByGroup('validation', 'fr'); // only rows that have a French value // Raw Eloquent collection Lingua::translations();
Writing & deleting translations
// Set a translation value (creates or updates; uses current locale if omitted) Lingua::setTranslation('welcome', 'Bienvenue', 'fr'); Lingua::setTranslation('welcome', 'Welcome'); // current locale // Remove a locale's value from a translation key // If the locale is the default, the entire record is deleted Lingua::forgetTranslation('welcome', 'fr'); Lingua::forgetTranslation('welcome'); // current locale
Language lifecycle
// Install language files for a locale via laravel-lang (lang:add) Lingua::addLanguage('fr'); // Remove language files for a locale via laravel-lang (lang:rm --force) Lingua::removeLanguage('fr');
Note:
addLanguage()andremoveLanguage()only manage language files on disk. Database records (Language model) and translation rows are handled separately — use the Artisan commandslingua:add/lingua:removefor a fully orchestrated operation that covers files, DB records, and sync in one step.
Sync
// Import all local lang/ files into the database Lingua::syncToDatabase(); // Export all database translations to local lang/ files Lingua::syncToLocal();
🏗 Architecture
How translations are stored
Lingua stores translations in the language_lines table, extending
Spatie's laravel-translation-loader. Each row holds all
locales in a single JSON text column, eliminating the need for per-locale rows:
group | key | text
------------|--------------|--------------------------------------------------------------
validation | required | {"en": "The :attribute field is required.", "it": "..."}
single | Welcome | {"en": "Welcome", "fr": "Bienvenue", "de": "Willkommen"}
This design allows instant locale switching at runtime without additional queries per language.
Translation types
Each string is classified automatically during sync:
| Type | Use case | Auto-detected when… |
|---|---|---|
text |
Plain strings, labels, messages | Default |
html |
Rich content with HTML markup | String contains HTML tags |
markdown |
Markdown-formatted content | String parses as Markdown |
The type drives which editor is shown in the Translations UI.
Bi-directional sync
lang/en/*.php ─┐
lang/en.json │ lingua:sync-to-database → language_lines (DB)
lang/it/*.php │
lang/it.json ─┤
lang/vendor/… │ ← lingua:sync-to-local
─┘
sync-to-database— reads every locale file (core + vendor packages) and upserts rows inlanguage_lines, auto-creatinglanguagesrecords for any new locales discovered.sync-to-local— reads every row inlanguage_linesand writes locale-specific PHP/JSON files back tolang/, including vendor subdirectories.
Translation loading at runtime
Lingua registers a custom LinguaManager as the Laravel translation loader. At runtime it merges:
- File-based translations from
lang/ - Database translations via Spatie's
Dbloader
Database translations take precedence, enabling live overrides without touching source files.
Locale middleware
LinguaMiddleware is automatically appended to the web middleware group on boot. It:
- Reads the active locale from the session (
lingua.session_variable) - Falls back to the database default language
- Calls
app()->setLocale()and stores the locale in the session for the next request
🧪 Testing
# Run the full test suite composer test # Run with coverage report composer test-coverage
The suite uses Pest and covers:
- All 5 Artisan commands — happy paths and error handling
- All Livewire components — rendering, interactions, and event dispatching
- Bi-directional sync operations
- All blade components
- Helper functions
🤝 Contributing
Contributions are welcome! Please open an issue first to discuss your proposed change, then submit a PR. Run
composer lint before pushing.
📄 License
The MIT License (MIT). Please see LICENSE.md for more information.
Built with ❤️ by Alessandro Rivolta
Powered by Laravel · Livewire · Flux · Laravel Lang · Spatie