dashed/dashed-translations
Dashed Translations is a Laravel package for managing multilingual application strings. It helps organize and maintain translation files, making it easier to keep localization consistent across your project’s supported languages.
Install and register
composer require dashed/dashed-translations
Laravel’s package discovery should register the service provider. If it doesn’t (or you’ve disabled discovery), add the provider to bootstrap/providers.php / config/app.php.
Publish and migrate
php artisan vendor:publish --tag="dashed-translations-config"
php artisan vendor:publish --tag="dashed-translations-migrations"
php artisan migrate
Read the published config/dashed-translations.php before doing anything else — it will tell you the table name, the default locale(s), the group/namespace strategy, and whether the package replaces or augments Laravel’s file-based loader.
Look at these first, in order
config/dashed-translations.php — locales, table, cache settings, loader strategy.locale, group, key, value-style columns) and therefore what scoping you get for free.translation.loader or decorates Illuminate\Translation\Translator, all of Laravel’s __(), trans(), @lang, and trans_choice() calls flow through it automatically.import, export, sync, cache).First real use case Take one existing Blade view with hardcoded copy and convert it:
{{ __('dashboard.greeting', ['name' => $user->name]) }}
Insert the corresponding row (via the DashedCMS admin, an artisan command, or a seeder), switch locale with App::setLocale('fr'), and reload. If the string changes, the loader is wired correctly and you can scale the migration of strings from there.
DB-backed UI strings, file-based fallbacks
The package is most valuable when it becomes the primary translation source for user-facing strings while you keep lang/en/*.php in the repo for developer-authored defaults and package translations. Seed the DB from those files once, then let editors own them. Treat the files as the “source of truth for keys,” the DB as “source of truth for values.”
Separate concerns: UI strings vs. model content
Don’t use the translations table for translatable models (products, pages, categories). Model content belongs in per-model translation tables with a HasTranslations-style trait. Use dashed/dashed-translations for the interface chrome — nav labels, button text, flash messages, validation messages. Mixing the two creates a table that’s half CMS content and half framework strings, and neither team can reason about it.
Locale resolution middleware Standardize on one precedence order and encode it in middleware, not in controllers:
// 1. explicit route parameter / query (?lang=fr)
// 2. authenticated user preference
// 3. session
// 4. Accept-Language header
// 5. config('app.fallback_locale')
Register it in the web group before anything that reads translations (including the ShareErrorsFromSession and auth middleware if your auth screens are localized).
Import/export as a deploy step, not an editor feature The highest-leverage workflow is round-tripping the DB through version control:
php artisan translations:export --format=json # writes lang/*.json
# ... code review, merge, deploy ...
php artisan translations:import --force # loads back into DB
This gives you diffs, rollbacks, and PR review for copy changes without giving up the database as the runtime source. Run export in CI to detect drift between DB and repo.
Frontend hand-off For Vue/React/Inertia, don’t call the translation endpoint per string. Emit one JSON blob per locale at boot (or via an Inertia shared prop on locale change):
Inertia::share('translations', fn () => cache()->rememberForever(
"translations.".app()->getLocale(),
fn () => Translation::forLocale(app()->getLocale())->pluck('value', 'key')
));
Then a tiny t('key', {name}) helper on the JS side with {placeholder} replacement. Keep the placeholder syntax identical to Laravel’s :name convention or you’ll maintain two formatters forever.
Caching Almost every package like this ships with a full-table cache keyed by locale. Wire it into your deployment:
php artisan translations:cache # warm after migrate
php artisan optimize:clear # bust when config changes
Add cache busting to the model’s saved/deleted events if the package doesn’t already, and invalidate on import so a deploy that syncs translations doesn’t serve stale strings.
Testing
public function test_french_dashboard(): void
{
$this->seed(TranslationSeeder::class);
$this->withSession(['locale' => 'fr'])->get('/dashboard')
->assertSee('Tableau de bord');
}
Seed a minimal set of keys in tests rather than the whole production table — it keeps tests fast and forces you to notice when a view starts depending on a key nobody documented.
lang/*.php files and you’ll spend an hour debugging “I edited the row and nothing changed.” Confirm in the service provider whether it replaces translation.loader or chains behind it, and check with Lang::get('some.key') after php artisan config:clear.config:cache will freeze locale lists. If locales are read from config, adding a language requires a redeploy + config:cache refresh. If they’re read from the DB, make sure that query isn’t itself happening inside a config file.__() returns the key itself, which looks like a translation to a non-technical reviewer. Log misses in non-production (Lang::listen or a missing-key handler) or you’ll ship screens full of dashboard.greeting.utf8mb4. A composite unique index on (locale, group, key) can blow past the 767/3072-byte limit if the key column is a plain VARCHAR(255). Expect to trim to 191 chars or index a hash. This bites on MySQL 5.7 specifically.trans_choice() relies on the locale’s plural rules in Illuminate\Translation\Pluralizer, not on your rows. Store the pipe-delimited forms (apple|apples) as the value, but don’t expect per-locale rule customization from the package.<strong> into the DB. In Blade, {{ __('key') }} escapes it — good. If you need markup, {!! !!} is now an XSS surface controllable by whoever has admin access. Prefer separate “key + rich text variant” rows over unescaping arbitrary values.SetLocale middleware registered after ShareErrorsFromSession or an auth middleware that renders localized messages will produce mixed-language responses. Put it early in the web group.migrate → translations:import (if you import) → translations:cache → optimize. Running optimize first caches an empty translation table and the site renders keys until the next request that clears it.Translator decorator rather than a full replacement, that’s the cleanest place to add per-tenant scoping without forking.composer.json ("dashed/dashed-translations": "^1.0"), and if you’re relying on it in production, copy the migrations into your own database/migrations and treat the table as yours — that gives you an escape hatch if the package stalls, since the schema is the only thing the rest of your app actually depends on.How can I help you explore Laravel packages today?