thepinecode/i18n
A Laravel internationalization package for managing translations in the database, switching locales at runtime, localizing routes, and using simple helpers and Blade directives—without scattering language files across your app.
Install the package
composer require conedevelopment/i18n
Note: the upstream repo is conedevelopment/i18n — if your composer.json references an older vendor name, confirm against Packagist before installing. As of v1.6.0 the package supports Laravel 13.x (added in PR #44); it's a compatibility-only release, so the install constraint is the only thing that changed.
Publish the config (and any front-end stub the package ships)
php artisan vendor:publish --provider="Conedevelopment\I18n\I18nServiceProvider"
Look inside config/i18n.php first — it typically controls which translation groups are exported, the target file/endpoint, and the JS global name. Confirm the exact provider namespace from the package's README, since vendor renames have happened.
Wire it into your layout — either a Blade directive the package registers (e.g. @i18n / @translations) or a generated JS asset. Check the package's README/service provider boot() for the exact macro/directive name; drop it into resources/views/layouts/app.blade.php just before your JS bundle.
First use case — replace a hardcoded JS string with a lookup:
// before
alert('Are you sure?');
// after
alert(__('messages.confirm'));
The __() helper (or whatever global the package exposes) mirrors Laravel's __() signature, including :attribute-style parameter replacement.
Verify by dumping the global in the browser console (window.translations / window.i18n) — if the object is empty, the export step or config groups are the culprit.
Check your Laravel version constraint alongside the package. v1.6.0 is the first release advertising Laravel 13 compatibility; if you're still on Laravel 12 or older, v1.5.x remains a valid pin and there's no behavioural reason to rush the upgrade.
Server-rendered translations (default, simplest). The package injects the current locale's translations into the page via a Blade directive. This is the pattern to reach for when you have a single page load and a fixed locale — no extra request, works with SSR, zero flicker. Prefer this for admin panels, marketing pages, and anything with modest translation volume.
Lazy/async loading for locale switching. For SPAs or pages with a language switcher, expose the package's export endpoint (or generated per-locale JSON files) and fetch the new bundle on switch:
async function switchLocale(locale) {
const res = await fetch(`/i18n/${locale}.json`);
window.i18n = await res.json();
document.documentElement.lang = locale;
// then re-render your app (Pinia/Vuex/React state)
}
Pair this with a small reactive wrapper — Alpine store, Pinia store, or React context — that wraps the package's lookup function so components re-render on locale change. The package's global is a plain object, not reactive; you must wrap it yourself.
Framework integration (Vue/React/Alpine). Keep a thin adapter so you don't sprinkle the global everywhere — one file you can swap if the package changes:
// i18n.js
export const t = (key, replacements = {}) =>
window.__?.(key, replacements) ?? key;
Then import t throughout components. This also gives you a natural place to add instrumentation for missing keys in dev.
Blade → JS bridge for dynamic strings.
Anything that needs runtime values (user names, IDs) should come from the translation string with placeholders, not string concatenation — the package respects Laravel's :name replacement, so use __('greeting', { name: user.name }) client-side rather than __('greeting_prefix') + ' ' + user.name, otherwise word order breaks in RTL/RTL-adjacent languages.
CI/export pattern.
In your deploy pipeline, run the package's export command as part of the build (php artisan i18n:export or equivalent) after config:cache, since the export usually reads from the cached config and compiled view paths. Commit the generated JSON if you serve it statically; otherwise generate at deploy time.
Upgrade pattern for compatibility-only releases.
Releases like v1.6.0 (a single "Laravel 13.x Compatibility" PR) carry no runtime behaviour change. Treat them as: bump the constraint → run your app's test suite against the new Laravel major in CI → verify the rendered bundle is byte-identical (no re-export needed). Keep the bump in its own commit so a framework upgrade is separable from a translation-layer upgrade.
Regeneration is not automatic. Translations are usually exported by an artisan command or a service-provider boot hook that writes a file/endpoint cache. If you add a key to lang/en/messages.php and it doesn't appear client-side, that's almost always a stale export — rerun the command (and clear config:cache).
Locale must match server-side. The package typically reads App::getLocale() at render time. If you resolve locale from a cookie/session, do it in a middleware that runs before the layout renders — otherwise the injected bundle will be in the fallback locale while the rest of the page is fr.
Config cache traps. Anything you tune in config/i18n.php (groups, namespaces, target path) is captured by php artisan config:cache. Editing the config without re-caching is a common "why is my change ignored" moment in production.
Missing-key behavior differs from Laravel. In PHP, __('missing.key') returns the key itself. The JS helper may return undefined, an empty string, or the key depending on version — add a dev-only warning in your adapter so missing translations surface in the console instead of silently rendering blank UI.
Escaping. Translations injected via a Blade directive are HTML-escaped or JSON-encoded depending on the package's implementation. If a translation contains quotes, apostrophes, or </script>, test it explicitly — French and Italian strings with apostrophes are the classic breakage.
Volume. Exporting all groups for all namespaces creates a fat inline object and inflates the HTML. Scope the config to the namespaces you actually use in JS (usually messages and maybe validation), and leave server-only translation files out entirely.
Extension points. Most of these packages ship a contract (e.g. TranslationLoader) bound in the service provider — if you need to strip unused keys, normalize placeholders, or pull from a database instead of lang/, rebind that contract in a ServiceProvider::register() rather than forking the package.
Testing. Because the client lookup goes through a global, unit tests usually stub window.__ (or use globalThis) with your adapter's t function. If the package exposes the resolver directly, mock that instead so your tests stay decoupled from the global name.
Version pinning and Laravel support windows. This is a small package, so treat each release as potentially significant: pin the exact version in composer.json and read the changelog before upgrading. The important signal in v1.6.0 is the supported Laravel range — a major-version compatibility bump means the package tracks Laravel majors, so pin the package and your laravel/framework constraint together, and re-check compatibility at each Laravel upgrade rather than assuming forward compatibility. Keep your JS-global adapter (see above) as the blast radius if a future minor release renames the global or changes config keys.
How can I help you explore Laravel packages today?