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 Translation Laravel Package

joedixon/laravel-translation

A Laravel package for managing app translations through a built-in web UI. Browse and edit language strings, add new locales, and keep translations organized using file or database drivers.

View on GitHub
Deep Wiki
Context7

Getting Started

This package swaps Laravel's file-based translator for a database-backed one and gives you a small admin UI for editing strings at runtime. Get it running in ~5 minutes:

composer require joedixon/laravel-translation
php artisan vendor:publish --provider="JoeDixon\Translation\TranslationServiceProvider"
php artisan migrate

Then open /translations (configurable via translation.route.prefix). Add a language, add a group/key, and __('group.key') starts resolving from the DB with a fallback to lang/.

Where to look first, in order:

  1. config/translation.php — the route.prefix, route.middleware, and enabled flags are the knobs you'll actually touch. Confirm the enabled flag is respected before wiring up your routes.
  2. JoeDixon\Translation\TranslationServiceProvider — it rebinds the translator singleton to JoeDixon\Translation\Translator. Understanding this one line explains 80% of the surprises (see Gotchas).
  3. JoeDixon\Translation\Drivers\Database — the class that does the reads/writes. Read its getTranslations() and addTranslations() methods; they define the caching and lookup behaviour you'll be debugging.
  4. php artisan list | grep translation — the package ships artisan commands (notably the sync command, translation:sync in current versions). This is how you get your existing lang/ files into the DB.

Minimal first use case: a marketing or internal app where non-developers need to fix copy ("Sign up today" → "Start your free trial") without a deploy. Sync your files once, hand the UI to a content owner behind auth, and stop shipping PRs for text changes.

Before you commit, check the composer.json constraint on illuminate/support. The last release was April 2023, so verify Laravel 11/12 compatibility explicitly or plan to fork.

Implementation Patterns

DB as source of truth, files as a mirror. The most common production pattern is: files remain the canonical definition of which keys exist, the DB is canonical for values in editable locales. Run the sync command as part of your release pipeline so new keys land in the DB, then periodically export the DB back to lang/ files and commit them. Without that export loop, a migrate:fresh in staging silently wipes every copy edit your translators made.

Seed a baseline for tests and CI. Write a seeder that inserts the languages and a starter set of translations, and run it after migrate in CI. Otherwise your test suite passes locally (where you've clicked around in the UI) and fails on a clean checkout, because __() returns raw keys.

// database/seeders/TranslationSeeder.php
public function run(): void
{
    $en = Language::create(['name' => 'English', 'language' => 'en']);
    // or call the sync command directly
    $this->call('translation:sync');
}

Gate the UI immediately. The default middleware is web — meaning anyone who guesses /translations can rewrite your entire application's copy. Change translation.route.middleware to include auth plus a gate before you deploy anywhere real:

'route' => [
    'prefix'     => 'translations',
    'middleware' => ['web', 'auth', 'can:manage-translations'],
],

Custom models for scoping. If you're multi-tenant, there is no tenant column in the schema. Override the package models via config (check config/translation.php for the model bindings) and add a global scope, or run separate databases per tenant. Bolting tenancy on after the fact means backfilling every translation row.

Locale negotiation on top of Language. The package uses the Language table for available locales and stores the chosen one in session. Pair it with a small middleware that reads Accept-Language on first visit and writes the choice to session, falling back to config('app.locale'). Be careful where this sits in the stack relative to anything calling __() early.

Cache warming in the deploy step. If the driver loads a locale's translations wholesale, warm that cache as part of deploy rather than letting the first user pay the cost:

php artisan translation:sync
php artisan cache:forget translations.en   # exact key depends on version

Frontend/SPA translations. The DB is server-side only. If you're using Vue/React i18n, write a tiny artisan command that reads the Translation model and dumps resources/js/lang/{locale}.json, then run it after any UI edit. Don't let the two systems drift.

Alternatives worth benchmarking against: spatie/laravel-translation-loader gives you the same DB-backed translator with a cleaner driver abstraction and better maintenance; it lacks the UI, which is exactly what this package buys you.

Gotchas and Tips

The translator rebind is global and order-sensitive. If another package (Nova, any translation-related addon, your own AppServiceProvider binding) also rebinds translator, whoever runs last wins. Symptom: your UI edits stop appearing, or the package's UI ignores file translations. Register the package's provider last and don't touch the translator binding elsewhere.

Fresh installs and config:cache can explode. The DB driver resolves the translator and queries translations / languages. On a fresh clone before migrate, or during php artisan config:cache in a broken DB state, this surfaces as a Base table or view not found error from an unrelated command. Fix: guard the driver with a Schema::hasTable() check in a custom driver, or wrap your bootstrap in a try/catch.

enabled + route caching. Flipping translation.enabled alone does nothing if routes are cached — the package registers its routes conditionally. Always php artisan route:clear after toggling. Same for config changes generally: config:clear is your first debugging move.

Missing keys and dynamic keys. The sync command discovers keys by statically scanning lang/ files (and in some versions resources/views). Anything built at runtime — __("status.{$status}"), keys from a config array, strings pulled from an API — will never appear in the UI. Add those manually or generate them in a seeder. Conversely, keys removed from code but present in the DB linger forever; there's no garbage collection.

Sync is not a merge. Depending on version and flags, translation:sync can overwrite or duplicate existing rows. Always dry-run on a staging copy with a database dump first. If the command supports an overwrite flag, leave it off in production — it will happily clobber copy your translators spent weeks on.

Pluralization and trans_choice. The DB driver stores the raw value including the | separators, so trans_choice generally works — but verify with a real test, because a translator pasting a translated string with a stray | breaks the count logic silently. Also confirm :attribute and :count placeholders survive; they should since the value is passed through untouched, but check your version.

HTML in translation values. The admin UI is a plain form; whatever a translator pastes is rendered raw in your Blade templates. If the UI doesn't escape on input, treat translation values as untrusted content — escape on output ({{ }}, not {!! !!}) or you've created a stored-XSS vector reachable by anyone with access to /translations.

Testing friction. The translator caches loaded groups in memory per locale. Between tests with RefreshDatabase, you can get stale strings from a previous test. Clear the translator/cache in setUp() or tearDown():

protected function setUp(): void
{
    parent::setUp();
    app('translator')->setLoaded([]); // or Cache::flush() depending on version
}

Deploy ordering matters. migrate before translation:sync, and both before any cache warming. If you sync first on a fresh database, the command fails on a missing languages table — a common CI red herring.

Performance indexing. Add a composite index on (language_id, group) if it isn't in the published migration. Wholesale per-locale loading is cheap once, expensive on every cold cache hit; a missing index turns that into a slow full scan on a fat translations table (validation/pagination/auth lang files alone can be thousands of rows).

Long-term maintenance risk. Last release 2023, 714 stars, no obvious recent activity. Budget for vendoring the package or forking it — the translator driver is ~300 lines and the migration path to spatie/laravel-translation-loader is straightforward if you keep values in the same table shape. Don't let this become load-bearing infrastructure a year from now without a plan.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource