Use your Laravel PHP translations directly in React, Vue, and Svelte — with full Vite HMR, namespace code-splitting, CDN overrides, and type safety.
Quick Start | How It Works | React / Vue / Svelte | CLI | ESLint | VSCode
Laravel has a great translation system. But when your frontend lives in React, Vue, or Svelte, you end up duplicating strings in JSON, manually syncing keys, and losing the structure Laravel gives you.
This plugin bridges the gap. Write translations in PHP the Laravel way, and use them in your frontend with zero duplication, automatic code-splitting, and full HMR.
.php file, see the change instantly:name parameters work exactly like Bladeinit, codemod (migrate from __()), and doctornpm install @zivex/laravel-vite-translations
# or
pnpm add @zivex/laravel-vite-translations
# or
bun add @zivex/laravel-vite-translations
// vite.config.ts
import { defineConfig } from "vite";
import translations from "@zivex/laravel-vite-translations";
export default defineConfig({
plugins: [
translations({
defaultLocale: "en",
// Optional: override auto-detected tooling
packageManager: "bun",
runtime: "bun",
}),
// ... your framework plugin (react/vue/svelte)
],
});
Package manager and runtime are auto-detected from package.json, lockfiles, the current user agent, and Bun runtime signals. Only set packageManager or runtime when you want to override detection manually.
// lang/en/dashboard.php
<?php
return [
'title' => 'Dashboard',
'welcome' => 'Welcome, :name!',
'stats' => [
'total' => 'Total Users',
'active' => 'Active Users',
],
];
// React
const { t } = useTranslations();
t("dashboard.title"); // "Dashboard"
t("dashboard.welcome", { name: "Taylor" }); // "Welcome, Taylor!"
t("dashboard.stats.total"); // "Total Users"
That's it. The plugin handles everything else — parsing PHP, generating JSON chunks, injecting imports, and loading only what's needed.
lang/en/dashboard.php ─────┐
lang/en/billing.php ───────┤
lang/nl/dashboard.php ─────┤
▼
┌─────────────────┐
│ PHP Parser + │
│ Generator │ buildStart / watch
└────────┬────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
en/dashboard en/billing nl/dashboard ← JSON chunks
│ │ │
└─────────────┼─────────────┘
▼
┌─────────────────┐
│ SWC Transform │ Finds t() calls in your code
└────────┬────────┘
│
Injects: import "virtual:lvt/dashboard"
│
▼
┌─────────────────┐
│ Runtime │ t() → override? → local → interpolate
└─────────────────┘
Key design decisions:
php-parser (full AST, not regex)t() calls; magic-string injects the importstranslations({
// Directories to scan for .php translation files
langPaths: ["lang"],
// Where to write generated JSON chunks
outputDir: "resources/js/lang/translations",
// Default locale for the transform
defaultLocale: "en",
// Optional: override auto-detected tooling
packageManager: "bun",
runtime: "bun",
// CDN URL for runtime overrides (optional)
cdnUrl: "https://cdn.example.com/translations",
// Generate a .d.ts file with all translation keys
generateTypes: true,
// Where to write the .d.ts file
typesOutputPath: "resources/js/lang/translations.d.ts",
// Additional glob patterns for translation directories
additionalPatterns: ["custom/*/lang/{locale}/*.php"],
});
// main.tsx
import { TranslationProvider } from "@zivex/laravel-vite-translations/react";
createRoot(document.getElementById("root")!).render(
<TranslationProvider locale="en" fallbackLocale="en">
<App />
</TranslationProvider>
);
// components/Dashboard.tsx
import { useTranslations } from "@zivex/laravel-vite-translations/react";
export function Dashboard() {
const { t, locale, setLocale } = useTranslations();
return (
<div>
<h1>{t("dashboard.title")}</h1>
<p>{t("dashboard.welcome", { name: "Taylor" })}</p>
<button onClick={() => setLocale("nl")}>Nederlands</button>
</div>
);
}
The useTranslations hook uses useSyncExternalStore under the hood — locale changes trigger re-renders efficiently without context cascading.
// main.ts
import { createApp } from "vue";
import { createTranslationsPlugin } from "@zivex/laravel-vite-translations/vue";
import App from "./App.vue";
const app = createApp(App);
app.use(createTranslationsPlugin({ locale: "en", fallbackLocale: "en" }));
app.mount("#app");
<!-- components/Dashboard.vue -->
<script setup lang="ts">
import { useTranslations } from "@zivex/laravel-vite-translations/vue";
const { t, locale, setLocale } = useTranslations();
</script>
<template>
<h1>{{ t("dashboard.title") }}</h1>
<p>{{ t("dashboard.welcome", { name: "Taylor" }) }}</p>
<p>Current locale: {{ locale }}</p>
<button @click="setLocale('nl')">Nederlands</button>
</template>
The Vue adapter uses provide/inject and reactive ref for locale tracking. $t is also available as a global property in templates.
<!-- App.svelte -->
<script lang="ts">
import { createTranslations } from "@zivex/laravel-vite-translations/svelte";
const { t, locale } = createTranslations({
locale: "en",
fallbackLocale: "en",
});
</script>
<h1>{$t("dashboard.title")}</h1>
<p>{$t("dashboard.welcome", { name: "Taylor" })}</p>
<p>Current locale: {$locale}</p>
<button on:click={() => locale.set("nl")}>Nederlands</button>
The Svelte adapter exposes t as a derived store and locale as a writable store — use them with the $ syntax as you would any Svelte store.
For cases where you need the raw runtime without a framework adapter:
import { createI18n } from "@zivex/laravel-vite-translations/runtime";
const { t, setLocale, getLocale, onLocaleChange } = createI18n({
locale: "en",
fallbackLocale: "en",
cdnUrl: "https://cdn.example.com/translations", // optional
});
t("dashboard.title");
t("dashboard.welcome", { name: "Taylor" });
await setLocale("nl");
const unsubscribe = onLocaleChange((locale) => {
console.log("Locale changed to", locale);
});
When no explicit locale is provided, the runtime detects it automatically:
localStorage (lvt-locale key)<html lang="..."> attributenavigator.language"en")Ship translation fixes without redeploying your app. Override translations are fetched from a CDN and take priority over local JSON:
translations({
cdnUrl: "https://cdn.example.com/translations",
});
The CDN should serve JSON files at {cdnUrl}/{locale}.json:
{
"dashboard.title": "Updated Dashboard Title"
}
Overrides are cached in memory, retried with exponential backoff on failure, and refreshed on locale change.
The package includes a CLI for project setup, migration, and diagnostics.
npx laravel-vite-translations <command>
# or
bunx laravel-vite-translations <command>
initScaffolds the plugin into your project:
npx laravel-vite-translations init
# or
bunx laravel-vite-translations init
vite.config.ts to add the pluginresources/js/lang/index.tsOptions: --locale <locale>, --framework <react|vue|svelte>, --lang-path <path>, --package-manager <auto|bun|pnpm|npm|yarn>, --runtime <auto|bun|node>, --no-codemod
init auto-detects package manager and runtime from package.json.packageManager, lockfiles, the active user agent, and Bun runtime signals. Manual overrides take priority.
codemodMigrates existing __() and trans() calls to t():
npx laravel-vite-translations codemod
# or
bunx laravel-vite-translations codemod
- <h1>{__('dashboard.title')}</h1>
+ <h1>{t('dashboard.title')}</h1>
Uses SWC for accurate AST-based transforms (not regex). Adds the import { t } statement automatically.
Options: --dir <directory> (default: resources/js), --dry-run
doctorDiagnoses translation issues across your project:
npx laravel-vite-translations doctor
# or
bunx laravel-vite-translations doctor
Reports:
Options: --dir <directory>, --lang-path <path>, --json
Flat config compatible (ESLint v9+).
npm install eslint --save-dev
# or
bun add -d eslint
// eslint.config.js
import translations from "@zivex/laravel-vite-translations/eslint";
export default [
translations.configs.recommended,
// ... your other configs
];
laravel-vite-translations/no-hardcoded-textWarns on hardcoded text in JSX elements:
// bad
<h1>Create Project</h1>
// good
<h1>{t("projects.create_title")}</h1>
Ignores className, id, key, data-*, aria-*, and other non-display attributes.
laravel-vite-translations/valid-translation-keyErrors on translation keys that don't exist in the translation index:
// error: Unknown translation key "dashboard.typo"
t("dashboard.typo");
Options: { indexPath: "path/to/translation-index.json" }
Available on the VS Marketplace as Laravel Vite Translations.
t("...") with translation value previewst("dashboard.title") to the PHP source fileThe extension reads the generated translation-index.json and watches for changes automatically.
| Setting | Default | Description |
|---|---|---|
laravelViteTranslations.generatedDir |
resources/js/lang/translations |
Path to generated translation files |
The plugin generates a .d.ts file with a union type of all your translation keys:
// Auto-generated
declare module "@zivex/laravel-vite-translations/runtime" {
export type TranslationKey =
| "dashboard.title"
| "dashboard.welcome"
| "dashboard.stats.total"
| "dashboard.stats.active"
| "billing.invoice";
export function createI18n(options?: I18nOptions): I18nInstance;
}
This gives you autocomplete and type checking on every t() call. The file is regenerated on every build and PHP file change.
The plugin scans these directories by default:
lang/{locale}/*.php # Laravel 9+
resources/lang/{locale}/*.php # Laravel 8
packages/*/lang/{locale}/*.php # Package translations
modules/*/lang/{locale}/*.php # Modular monolith
Namespace is derived from the PHP filename: lang/en/dashboard.php becomes the dashboard namespace, so keys are accessed as t("dashboard.key").
Nested PHP arrays are flattened with dot notation:
// lang/en/dashboard.php
return [
'stats' => [
'total' => 'Total Users', // → t("dashboard.stats.total")
],
];
Designed for large Laravel applications with hundreds of pages and thousands of translation keys.
| Optimization | What it does |
|---|---|
| Transform cache | Hashes source files, skips SWC re-parsing when unchanged |
| Namespace splitting | Each PHP file becomes a separate JSON chunk — only used namespaces are loaded |
| Generator diffing | Only rewrites JSON files that actually changed, preventing unnecessary HMR |
| Preload | <link rel="modulepreload"> for critical translation chunks |
| CDN caching | Override responses cached in memory, refreshed only on locale change |
For critical above-the-fold content, preload translation chunks to avoid waterfalls:
import { getPreloadLinks } from "@zivex/laravel-vite-translations/preload";
// In your SSR template or HTML
const links = getPreloadLinks(["dashboard", "nav"], "en");
// <link rel="modulepreload" href="/assets/en/dashboard.json" />
// <link rel="modulepreload" href="/assets/en/nav.json" />
| Path | Description |
|---|---|
@zivex/laravel-vite-translations |
Vite plugin |
@zivex/laravel-vite-translations/runtime |
Runtime API (createI18n, t) |
@zivex/laravel-vite-translations/react |
React adapter (TranslationProvider, useTranslations) |
@zivex/laravel-vite-translations/vue |
Vue adapter (createTranslationsPlugin, useTranslations) |
@zivex/laravel-vite-translations/svelte |
Svelte adapter (createTranslations) |
@zivex/laravel-vite-translations/eslint |
ESLint plugin |
@zivex/laravel-vite-translations/preload |
SSR preload utilities |
Publish the npm package:
pnpm release:npm
Publish the VS Code extension:
pnpm release:vscode
Run the full release flow in one command:
pnpm release:all
Package the VS Code extension locally without publishing:
pnpm release:vscode:package
A manual workflow is included at .github/workflows/release.yml.
Required repository secrets:
NPM_TOKEN for publishing @zivex/laravel-vite-translationsVSCE_PAT for publishing the VS Code extensionIf you want the npm package to stay under @zivex, the npm scope zivex must exist and your npm account must have publish access to it.