Vue 3 integration for Laravel Localizer - seamlessly use Laravel translations in your Vue 3/Inertia.js applications with full TypeScript support.
- 🎨 Vue 3 Composable -
useLocalizer()composable with Composition API - 🔌 Vite Plugin - Auto-regenerates TypeScript translations on file changes
- 🎯 TypeScript - Full type safety with TypeScript support
- ⚡ Inertia.js - Native integration with Inertia.js page props
- 🌐 Pluralization - Built-in pluralization support
- 🔄 Replacements - Dynamic placeholder replacement
- 🌍 RTL Support - Automatic text direction detection
- ⚛️ Reactive - Fully reactive locale and direction with Vue refs
- 📦 Tree-shakeable - Modern ESM build
- Vue 3.0+
- Inertia.js v1 or v2
- Laravel Localizer backend package
npm install @devwizard/laravel-localizer-vueFirst, install and configure the Laravel Localizer package:
composer require devwizardhq/laravel-localizer
php artisan localizer:installSee the Laravel Localizer documentation for complete backend setup.
First, generate TypeScript translation files from your Laravel app:
php artisan localizer:generate --allThis creates files like resources/js/lang/en.ts, resources/js/lang/fr.ts, etc.
Add the Vite plugin to auto-regenerate translations when language files change.
File: vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import laravel from 'laravel-vite-plugin';
import { laravelLocalizer } from '@devwizard/laravel-localizer-vue/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/js/app.ts'],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
laravelLocalizer({
// Watch patterns for language file changes
patterns: ['lang/**', 'resources/lang/**'],
// Command to run when files change
command: 'php artisan localizer:generate --all',
// Enable debug logging (optional)
debug: false,
}),
],
});What it does:
- Watches for changes in
lang/**andresources/lang/** - Automatically runs
php artisan localizer:generate --allwhen files change - Triggers HMR to reload your frontend with updated translations
Set up the global window.localizer object in your app entry point.
File: resources/js/app.ts
import './bootstrap';
import '../css/app.css';
import { createApp, h, DefineComponent } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
// Import all generated translation files
import * as translations from './lang';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob<DefineComponent>('./Pages/**/*.vue')
),
setup({ el, App, props, plugin }) {
// Initialize window.localizer with translations
if (typeof window !== 'undefined') {
window.localizer = {
translations,
};
}
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el);
},
progress: {
color: '#4B5563',
},
});TypeScript Declaration
To ensure type safety when accessing window.localizer, add this global declaration to your project:
declare global {
interface Window {
localizer: {
translations: typeof translations;
};
}
}Alternative: Create a separate file
File: resources/js/lang/index.ts
// Export all generated translations
export * from './en';
export * from './fr';
export * from './ar';
// ... add other locales as neededFile: resources/js/app.ts
import * as translations from './lang';
// ... in setup()
window.localizer = { translations };Add types to your tsconfig.json:
{
"compilerOptions": {
"types": ["@devwizard/laravel-localizer-vue"]
}
}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { __ } = useLocalizer();
</script>
<template>
<div>
<h1>{{ __('welcome') }}</h1>
<p>{{ __('validation.required') }}</p>
</div>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { __ } = useLocalizer();
const userName = 'John';
const itemCount = 5;
</script>
<template>
<div>
<!-- Supports :placeholder format -->
<p>{{ __('greeting', { name: userName }) }}</p>
<!-- "Hello :name!" → "Hello John!" -->
<!-- Also supports {placeholder} format -->
<p>{{ __('items', { count: itemCount }) }}</p>
<!-- "You have {count} items" → "You have 5 items" -->
</div>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { choice } = useLocalizer();
const count = ref(5);
</script>
<template>
<div>
<!-- Define in your translation file: -->
<!-- "apples": "no apples|one apple|many apples" -->
<p>{{ choice('apples', count) }}</p>
<!-- count = 0: "no apples" -->
<!-- count = 1: "one apple" -->
<!-- count = 5: "many apples" -->
<!-- With replacements -->
<p>{{ choice('apples', count, { count }) }}</p>
<!-- "You have {count} apples" → "You have 5 apples" -->
</div>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { __, has } = useLocalizer();
</script>
<template>
<div>
<h1 v-if="has('welcome')">{{ __('welcome') }}</h1>
<p v-if="has('custom.message')">
{{ __('custom.message') }}
</p>
<p v-else>Default message</p>
</div>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { __ } = useLocalizer();
</script>
<template>
<div>
<!-- Use fallback for missing keys -->
<p>{{ __('might.not.exist', {}, 'Default Text') }}</p>
</div>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { locale, dir, availableLocales } = useLocalizer();
// Note: locale and dir are ComputedRef, so use .value in script
</script>
<template>
<div :dir="dir">
<p>Current Locale: {{ locale }}</p>
<p>Text Direction: {{ dir }}</p>
<select :value="locale">
<option v-for="(meta, code) in availableLocales" :key="code" :value="code">
{{ meta.flag }} {{ meta.label }}
</option>
</select>
</div>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { __, dir } = useLocalizer();
</script>
<template>
<div :dir="dir" :class="dir === 'rtl' ? 'text-right' : 'text-left'">
<h1>{{ __('welcome') }}</h1>
<p>{{ __('description') }}</p>
</div>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { translations } = useLocalizer();
</script>
<template>
<div>
<h2>All Translations:</h2>
<pre>{{ JSON.stringify(translations, null, 2) }}</pre>
</div>
</template>
{% endraw %}Returns an object with the following properties and methods:
| Property | Type | Description |
|---|---|---|
__ |
(key: string, replacements?: Replacements, fallback?: string) => string |
Main translation function |
trans |
(key: string, replacements?: Replacements, fallback?: string) => string |
Alias for __() |
lang |
(key: string, replacements?: Replacements, fallback?: string) => string |
Alias for __() |
has |
(key: string) => boolean |
Check if translation key exists |
choice |
(key: string, count: number, replacements?: Replacements) => string |
Pluralization support |
locale |
ComputedRef<string> |
Reactive current locale code |
dir |
ComputedRef<'ltr' | 'rtl'> |
Reactive text direction |
availableLocales |
ComputedRef<Record<string, LocaleMeta>> |
Reactive available locales |
translations |
ComputedRef<Record<string, string>> |
Reactive all translations |
Note: Unlike React, locale, dir, availableLocales, and translations are ComputedRef values, making them fully reactive in Vue.
interface LocalizerOptions {
// Watch patterns for language file changes
patterns?: string[]; // default: ['lang/**', 'resources/lang/**']
// Command to run when files change
command?: string; // default: 'php artisan localizer:generate --all'
// Enable debug logging
debug?: boolean; // default: false
}The package is written in TypeScript and provides full type definitions:
import {
useLocalizer,
UseLocalizerReturn,
Replacements,
LocaleData,
PageProps,
} from '@devwizard/laravel-localizer-vue';
// All types are available for importThe package includes comprehensive tests using Vitest:
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run test:coverage{% raw %}
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { locale, availableLocales } = useLocalizer();
const changeLocale = (newLocale: string) => {
router.visit(route('locale.switch', { locale: newLocale }), {
preserveScroll: true,
preserveState: true,
});
};
</script>
<template>
<select :value="locale" @change="changeLocale($event.target.value)">
<option v-for="(meta, code) in availableLocales" :key="code" :value="code">
{{ meta.flag }} {{ meta.label }}
</option>
</select>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
import { useForm } from '@inertiajs/vue3';
const { __ } = useLocalizer();
const form = useForm({
email: '',
password: '',
});
const submit = () => {
form.post('/login');
};
</script>
<template>
<form @submit.prevent="submit">
<div>
<label>{{ __('auth.email') }}</label>
<input v-model="form.email" type="email" required />
<span v-if="form.errors.email" class="error">
{{ form.errors.email }}
</span>
</div>
<div>
<label>{{ __('auth.password') }}</label>
<input v-model="form.password" type="password" required />
<span v-if="form.errors.password" class="error">
{{ form.errors.password }}
</span>
</div>
<button type="submit" :disabled="form.processing">
{{ __('auth.login') }}
</button>
</form>
</template>
{% endraw %}{% raw %}
<script setup lang="ts">
import { computed } from 'vue';
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { __, locale, dir } = useLocalizer();
// locale and dir are reactive, so this will update automatically
const greeting = computed(() => {
return __('greeting', { locale: locale.value });
});
const containerClass = computed(() => ({
'text-right': dir.value === 'rtl',
'text-left': dir.value === 'ltr',
}));
</script>
<template>
<div :class="containerClass">
<h1>{{ greeting }}</h1>
<p>Direction: {{ dir }}</p>
</div>
</template>
{% endraw %}Here's a full example of a multilingual user dashboard:
Backend: lang/en.json
{
"welcome": "Welcome",
"dashboard": "Dashboard",
"greeting": "Hello, :name!",
"notifications": "You have :count notifications"
}Backend: lang/en/dashboard.php
<?php
return [
'title' => 'User Dashboard',
'stats' => [
'users' => '{0} No users|{1} One user|[2,*] :count users',
'posts' => 'You have :count posts',
],
];Generate translations:
php artisan localizer:generate --allFrontend: resources/js/Pages/Dashboard.vue
{% raw %}
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { PageProps } from '@/types';
interface DashboardProps extends PageProps {
stats: {
users: number;
posts: number;
notifications: number;
};
}
const props = defineProps<DashboardProps>();
const { __, choice, locale, dir } = useLocalizer();
</script>
<template>
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('dashboard.title') }}
</h2>
</template>
<Head :title="__('dashboard')" />
<div class="py-12" :dir="dir">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<!-- Greeting with replacement -->
<h1 class="text-2xl font-bold mb-4">
{{ __('greeting', { name: auth.user.name }) }}
</h1>
<!-- Notification count -->
<p class="mb-4">
{{ __('notifications', { count: stats.notifications }) }}
</p>
<!-- Statistics with pluralization -->
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-blue-50 rounded">
<h3 class="font-semibold">Users</h3>
<p>{{ choice('dashboard.stats.users', stats.users, { count: stats.users }) }}</p>
</div>
<div class="p-4 bg-green-50 rounded">
<h3 class="font-semibold">Posts</h3>
<p>{{ __('dashboard.stats.posts', { count: stats.posts }) }}</p>
</div>
</div>
<!-- Locale info -->
<div class="mt-4 text-sm text-gray-500">
<p>Current locale: {{ locale }}</p>
<p>Text direction: {{ dir }}</p>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
{% endraw %}Language Switcher Component:
{% raw %}
<script setup lang="ts">
import { useLocalizer } from '@devwizard/laravel-localizer-vue';
const { locale, availableLocales } = useLocalizer();
const switchLocale = (newLocale: string) => {
// Simple page reload with locale parameter
window.location.href = `${window.location.pathname}?locale=${newLocale}`;
};
</script>
<template>
<div class="relative">
<select
:value="locale"
@change="switchLocale(($event.target as HTMLSelectElement).value)"
class="block w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option v-for="(meta, code) in availableLocales" :key="code" :value="code">
{{ meta.flag }} {{ meta.label }}
</option>
</select>
</div>
</template>
{% endraw %}What happens:
- User changes locale in dropdown
- Page reloads with
?locale=frparameter - Laravel middleware detects locale and updates session
- Vue components re-render with new translations
- Text direction automatically adjusts for RTL languages
# Install dependencies
npm install
# Run tests
npm test
# Build the package
npm run build
# Run linter
npm run lint
# Format code
npm run formatContributions are welcome! Please see CONTRIBUTING for details.
Please see CHANGELOG for recent changes.
The MIT License (MIT). Please see License File for more information.
- Laravel Localizer - Backend package
- @devwizard/laravel-localizer-react - React integration