modera/languages-bundle
A Symfony bundle from Modera for managing application languages and locales. It helps define available languages, centralize localization configuration, and integrate language management into Modera-based admin and content workflows.
modera/languages-bundle is a Symfony bundle backed by Doctrine, last tagged in June 2016 and published as a read-only subtree split of the Modera monorepo. For a Laravel developer, "getting started" means one thing: read it as a spec, do not install it. There is no service provider, no publishable config, no migration set, and the Symfony 2/3 constraints will not resolve against a modern Laravel install.
Minimal path in a Laravel app (the recommended one):
Entity/Language.php (the mapped class — roughly id, code, name, enabled) and DependencyInjection/ (where the entity class parameter and service aliases live). That is the entire public surface; everything else is bundle wiring.Schema::create('languages', function (Blueprint $t) {
$t->increments('id');
$t->string('code', 5)->unique(); // ISO 639-1, e.g. "en"
$t->string('name'); // "English"
$t->boolean('enabled')->default(true);
$t->timestamps();
});
symfony/intl (Languages::getNames()) or umpirsky/language-list rather than hand-maintaining them.config/app.php / a dedicated config file, not in the table.First use case: a language switcher. Read enabled languages once, build a code => name map, cache it, and render the switcher off that map.
Only if you are in a legacy Symfony 2.x/3.x app: composer require modera/languages-bundle, register new Modera\LanguagesBundle\ModeraLanguagesBundle() in AppKernel, let Doctrine generate the schema, convert the dump to an explicit migration, and seed the ISO list yourself.
Thin model + cached repository. The day-to-day value is a single read-mostly lookup, so wrap it:
final class LanguageRepository
{
public function enabled(): Collection
{
return Cache::rememberForever('languages.enabled', fn () =>
Language::where('enabled', true)->orderBy('name')->get()
);
}
public function byCode(string $code): ?Language
{
return $this->enabled()->firstWhere('code', $code);
}
}
Bind it behind an interface (LanguageRepositoryInterface) in a service provider. That way, if you ever do need to swap the implementation, it's a container binding rather than a rewrite.
Locale negotiation middleware. This is the piece that actually earns its keep in a Laravel app:
public function handle(Request $request, Closure $next)
{
$available = app(LanguageRepositoryInterface::class)->enabled()->pluck('code')->all();
$locale = $request->getPreferredLanguage($available) ?? config('app.locale');
app()->setLocale($locale);
return $next($request);
}
Laravel's getPreferredLanguage() already parses Accept-Language including q-values — don't hand-roll this.
Seeder from CLDR data. One artisan seeder, idempotent (updateOrCreate on code), reading Languages::getNames(). Run it in every environment so the language list is never a manual DB step.
Switcher as a Blade component. Read enabled(), emit a form/select posting to a /locale/{code} route guarded by in_array($code, $available). Persist to session so the choice survives navigation, and apply it in the middleware before Accept-Language is consulted.
Per-language configuration. Where the Modera bundle implies default-language settings, do the same with a config array keyed by code (config/languages.php) for fallbacks or formatting overrides — but keep the enabled set in the database so admins can toggle without a deploy. Two sources of truth is fine as long as the split is: DB = which languages, config = how each behaves.
Cache invalidation on write. Hook Language::saved and Language::deleted to Cache::forget('languages.enabled'). Without that, an admin disables a language and the middleware keeps negotiating into it.
Testing pattern. Feature-test the middleware with $this->withHeader('Accept-Language', 'fr-CA,fr;q=0.9,en;q=0.5') and assert app()->getLocale(). Test the fallback path (request a disabled language, assert default) explicitly — it's the failure that shows up in production as wrong-language emails.
ServiceProvider, no facade, no publishable config. Any catalog listing it as Laravel-native is mislabeled. Budget zero time trying to composer require it.--ignore-platform-reqs buys a green build and an unverified runtime — don't.enabled filtering is yours to enforce everywhere. The negotiation middleware, the switcher, and any admin CRUD must each filter on it. A partial implementation lets users select a language the app can't serve.en is a language; en_US is a locale. The Modera entity is language-shaped. If you need regional formatting (dates, currency), add a separate locale column or derive it — don't overload code.toSql() before assuming.code to lowercase and enforce the unique index. EN and en in the same table will quietly break lookups.fr-CA → fr) → config('app.locale'). Anything that can produce an unset locale will surface as untranslated UI, which is customer-visible and easy to miss in tests.How can I help you explore Laravel packages today?