waavi/translation
Waavi Translation is a Laravel package that stores and manages translations in the database instead of flat files. It adds caching, file import/export, a translator service, and fallback support so you can edit language strings at runtime without redeploying.
1. Install and register
composer require waavi/translation
The package targets Laravel 5.x (illuminate/support ^5.x constraints), so pin a version that matches your framework and verify on a branch before merging. Register the provider in config/app.php:
Waavi\Translation\TranslationServiceProvider::class,
Optionally add the facade alias if you prefer Translator:: over trans()/app('translator').
2. Publish config and schema
php artisan vendor:publish --provider="Waavi\Translation\TranslationServiceProvider"
php artisan migrate
This gives you config/translator.php and a translations table. Read the config file top to bottom before doing anything else — it's short and controls locales, the loader chain, cache keys/TTL, and the model/repository bindings. Every behavior you'll later debug traces back to a key in that file.
3. Seed from your existing lang files
php artisan translator:import
This walks resources/lang (and vendor namespaces if configured) and writes every group.key into the database. After it runs, trans('messages.welcome') keeps working exactly as before — the difference is that the lookup now resolves through the package's loader chain instead of the file loader.
4. First use case worth validating end-to-end
Change one row's text in the translations table for a non-default locale, set that locale, and hit a page. Then run php artisan translator:flush (or whatever your configured flush command is named) and confirm the cache picks up the new value. If that round trip works, the package is wired correctly; if it doesn't, the problem is 95% of the time the cache layer or the loader binding.
Where to look first in the source: src/Translator.php (the delegating translator), src/Loaders/ (File / Database / Cached decorator), and src/Repositories/TranslationRepository.php (all read/write operations). Those three files are the whole mental model.
Decide early: files or database as source of truth
The package supports both, and mixing them silently is the main source of confusion.
resources/lang/**, CI runs translator:import on deploy, DB is a cache-ish mirror. Good for Git review of copy changes.translator:export dumps back to files for backup or for vendor consumption.Pick one, document it in the README of your app, and make the other direction a deliberate, reviewed command.
Use the repository for writes, not raw Eloquent
$repo = app(\Waavi\Translation\Repositories\TranslationRepository::class);
$repo->translate('messages', 'welcome', 'fr', 'Bienvenue');
Going through the repository is what keeps the cached loader coherent — a Translation::where(...)->update() call bypasses cache invalidation and gives you a stale locale for the TTL duration. If you must write directly (bulk import scripts), follow up with an explicit flush.
Build your own admin CRUD
The package ships controller/views for browsing translations in the browser, but in practice most teams disable the route group and write a small controller over TranslationRepository (list by locale + group, inline edit, "missing" filter). Reasons: the bundled UI has no auth, no audit trail, and assumes a specific layout. Reusing the repository means you get cache invalidation for free.
Missing-key capture
A high-value pattern for translation-heavy apps: in a middleware or a custom loader wrapper, detect keys that fell through to the fallback locale, log them with the request path, and later feed that list into the CRUD screen. The package's file loader returns the raw key on miss, so a wrapper that compares "returned string === requested key" is a cheap detector.
Locale switching middleware
Keep locale resolution in one middleware (Accept-Language header, user preference, session) and call App::setLocale() there. Do not scatter App::setLocale() calls — the cached loader is keyed per locale, and inconsistent switching makes cache behavior look random during debugging.
Testing
In phpunit.xml, set the loader to the file loader (or disable the database/cache layer) so unit tests don't depend on seeded rows. For integration tests that do exercise the DB path, seed a minimal fixture set in setUp() rather than importing the whole lang tree — imports of vendor namespaces can be thousands of rows and will dominate suite runtime.
Deploy script
php artisan migrate --force
php artisan translator:import # if files are the source of truth
php artisan translator:flush
php artisan config:cache
Order matters: flush after import, config cache last.
Cache invalidation is not automatic on every path. The cached loader is a decorator around the DB loader; it only refreshes when the repository flushes the relevant key. Any code path that mutates translations outside the repository — seeders, raw SQL, an old admin panel — will appear to "not save" until TTL expires. Make flush part of every write workflow, and consider a very short TTL in staging.
Composite cache keys must include locale and group. If you customize the cache key format, verify two locales of the same group don't collide. Symptom: users intermittently see another language's strings after a cache warm. This is the single most common bug report pattern for loader-decorator translation packages.
translator:import can overwrite manual DB edits. If your translators have been working in the database and someone runs an import from stale files, those edits are gone. Before deploying to production, take a dump of the translations table, and test the command's overwrite behavior on a copy — the exact semantics (insert-only vs upsert) depend on your version.
Empty strings shadow the fallback locale. A DB row with text = '' resolves to an empty string rather than falling through to fallback_locale, which produces blank UI labels that look like a rendering bug. Either treat empty as NULL on write, or normalize on read in your loader wrapper.
Index length on utf8mb4. The translations table has a composite index over locale/group/key (or similar). On MySQL with utf8mb4, older index-length limits can push key columns to 191 chars, and long dot-notation keys get truncated on insert. If keys silently fail to save, check the schema the migrations published — you may need to widen or shorten.
Vendor/package namespaces bloat the table. Importing vendor:: namespaces pulls in every string of every package. Usually you want the file loader to keep serving vendor namespaces and the DB loader to serve only your app's groups — check the loader configuration for a namespace filter rather than importing everything.
The file loader must stay in the chain. Even in full DB mode, keep the file loader as fallback for packages that ship their own lang files and were never imported; otherwise those strings render as raw keys.
Version/maintenance risk. This is a Laravel 5-era package with effectively no active maintenance and no dependents. Budget for an exit: the loader chain is easy to replace with a custom Illuminate\Contracts\Translation\Loader (or spatie/laravel-translation-loader) when you upgrade past Laravel 5. Keep your admin UI and repository calls thin so the swap is contained to the service provider binding.
Debugging checklist when a string won't change:
php artisan translator:flushphp artisan config:clear (a cached config with an old locale list is a classic)get_class(app('translator')->getLoader()) — decorators stack, so look at the innermost class.locale + group + key must match the trans key exactly, including namespace prefix.How can I help you explore Laravel packages today?