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

Expose Translation Bundle Laravel Package

hardchor/expose-translation-bundle

A small Symfony bundle that exposes your application's translation catalogs—useful for making message strings available to frontend JavaScript or external clients. Lightly adopted, so review the source for supported formats, routes, and integration details before relying on it in production.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the framing: this is a Symfony bundle, not a Laravel package. There is no service provider, no config publish, no facade. hardchor/expose-translation-bundle is a fork of willdurand/expose-translation-bundle (the BazingaExposeTranslationBundle), which was a Symfony 2-era bundle that solved one problem: dump your translation catalogue to the browser so JS can call Translator.trans('some.key') with the same strings your Twig templates use. As a Laravel developer your realistic options are (a) you're working in a legacy Symfony codebase that already has it, or (b) you want to steal the pattern. Treat adoption in new code as a non-starter — 1 star, 0 dependents, Symfony 2 constraints.

Minimal steps (Symfony 2/3 target):

composer require hardchor/expose-translation-bundle
// app/AppKernel.php
new Bazinga\ExposeTranslationBundle\BazingaExposeTranslationBundle(),
# app/config/routing.yml
bazinga_exposetranslation:
    resource: "@BazingaExposeTranslationBundle/Resources/config/routing.xml"
# app/config/config.yml
bazinga_expose_translation:
    default_domains: [messages]

Where to look first: open the dump route in a browser (/translations/messages.en.js or whatever prefix the imported routing gives you). That file is the whole contract — it emits a Translator.add('<domain>', { … }) call plus locale/fallback setup. Read it before reading any docs; the repo's README is thin and version drift between the fork and upstream is real.

First use case: put translator.min.js and the dump route in your layout after any asset that might call it, then in a page script:

Translator.trans('form.save', {name: 'Invoice'}, 'messages');

Mirror a Blade/Symfony form that already renders the same key server-side, and confirm both produce identical output in both locales. That's your smoke test.

Implementation Patterns

Load the runtime once, dump per page. The Translator JS object is a singleton; include the runtime in the layout and only the domain dump(s) the current page needs. Dumping every domain on every page is the usual cause of "why is my HTML 200KB".

<script src="{{ asset('bundles/bazingaexposetranslation/js/translator.min.js') }}"></script>
<script src="{{ url('bazinga_exposetranslation_js', {domain_name: 'javascript'}) }}"></script>

Use a dedicated javascript domain. Don't expose messages wholesale. Keep UI copy that only JS needs in its own file (translations/javascript.en.yml), so translators can edit it without touching server-rendered strings and you can ship it to every page cheaply.

Keep the dump out of your build pipeline. In Symfony this means Webpack Encore/Assetic; in Laravel terms it means Vite. The catalogue is locale-dependent at request time — if you bundle it you get a stale-locale bug that only shows up after a language switch. Pass the URL in instead:

<div id="app" data-translations-url="{{ url('bazinga_exposetranslation_js', {domain_name: 'javascript'}) }}"></div>

…and fetch it on boot in your SPA entry point, or just write the URL into a window.config object.

Placeholder reconciliation is your main integration task. Symfony messages use %name% (or {{ name }} in newer versions); Translator.js interpolates {name}. Either standardise your JS-facing domain on {name} and pass it through, or register a custom Translator filter on the JS side. Pick one and write it in the README of your app, because the failure is silent — you get a raw {name} on screen.

Server-side key validation. Grep your JS for Translator.trans('…') in a CI step and assert each key resolves via $translator->getCatalogue($locale)->has($key, $domain). This is the single highest-value thing you can add; see the gotchas below for why.

If you're actually on Laravel, don't port the bundle — port the shape. A single route returning response()->json(...) over Lang::get($domain) (or reading lang/{locale}/{domain}.php directly) plus a tiny t() helper gives you 95% of the value, and packages like laravel-js-localization already do it. Inertia users get it for free via shared props.

Caching: the dump is deterministic per (locale, domain). Set Cache-Control: public on it and let your reverse proxy do the work — an uncached translator dump on a high-traffic page is a measurable hit.

Gotchas and Tips

  • It almost certainly won't install on anything modern. Check the fork's composer.json for symfony/framework-bundle constraints before you plan around it. Symfony 4+ registered bundles in config/bundles.php; Symfony 2/3 used AppKernel. Neither is Laravel. If you're on Symfony 5/6, the fork is dead weight — write the 20-line controller yourself.
  • The global Translator is a namespace landmine. It collides with anything else exposing window.Translator, and if the dump route 404s you get Translator is not defined at the call site rather than at the script tag, which sends people hunting in the wrong file. Keep the runtime tag and dump tag adjacent.
  • Missing keys fail silently. Translator.trans('typo.here') returns the key string rather than throwing, so a typo ships to production as literal typo.here text. That's why the CI key check above matters.
  • Placeholder syntax mismatch (%name% vs {name}) is the #1 "it works in PHP but not JS" bug. Verify with a parameterised key first.
  • Escaping into a <script> context. Messages are emitted directly into JS. Quotes and newlines must be JSON-encoded correctly, and </script> inside a translation is an XSS vector in older versions. If translators (or a translation SaaS) can write messages, confirm the dump template escapes properly — read the template in Resources/views.
  • Locale resolution depends on your routing. The dump route keys off _locale. If your app doesn't put locale in the URL, the dump falls back to the request default and users see the wrong language on the JS side while the server-rendered page is fine. Test with an explicit _locale prefix.
  • default_domains: ~ exposes everything. Convenient in development, an information leak in production — internal/admin-only strings become visible in the JS.
  • Debugging checklist: hit the route directly with an explicit locale → confirm Translator.add contains your key → Translator.get('messages.key') in the console → Translator.locale matches → then blame the placeholder.
  • Extension point: the dumper is a controller service. Rather than fight the bundle, override the route to your own controller, call $translator->getCatalogue($locale), and json_encode it. That's the whole feature, and you own the escaping and cache headers.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core