Skip to content

Vue 3 integration for Laravel Localizer with Vite plugin, useTranslation composable, and automatic TypeScript generation

License

Notifications You must be signed in to change notification settings

DevWizardHQ/laravel-localizer-vue

Repository files navigation

@devwizard/laravel-localizer-vue

npm version npm downloads License: MIT

Vue 3 integration for Laravel Localizer - seamlessly use Laravel translations in your Vue 3/Inertia.js applications with full TypeScript support.

Features

  • 🎨 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

Requirements

  • Vue 3.0+
  • Inertia.js v1 or v2
  • Laravel Localizer backend package

Installation

npm install @devwizard/laravel-localizer-vue

Backend Setup

First, install and configure the Laravel Localizer package:

composer require devwizardhq/laravel-localizer
php artisan localizer:install

See the Laravel Localizer documentation for complete backend setup.

Setup

Step 1: Generate Translation Files

First, generate TypeScript translation files from your Laravel app:

php artisan localizer:generate --all

This creates files like resources/js/lang/en.ts, resources/js/lang/fr.ts, etc.

Step 2: Configure Vite Plugin

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/** and resources/lang/**
  • Automatically runs php artisan localizer:generate --all when files change
  • Triggers HMR to reload your frontend with updated translations

Step 3: Initialize Window 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 needed

File: resources/js/app.ts

import * as translations from './lang';

// ... in setup()
window.localizer = { translations };

3. Configure TypeScript (Optional)

Add types to your tsconfig.json:

{
  "compilerOptions": {
    "types": ["@devwizard/laravel-localizer-vue"]
  }
}

Usage

Basic Usage

{% 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 %}

With Replacements

{% 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 %}

Pluralization

{% 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 %}

Checking Translation Existence

{% 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 %}

With Fallback

{% 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 %}

Locale Information (Reactive)

{% 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 %}

RTL Support

{% 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 %}

Accessing All Translations

{% 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 %}

API Reference

useLocalizer()

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.

Vite Plugin Options

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
}

TypeScript Support

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 import

Testing

The 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

Examples

Language Switcher

{% 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 %}

Form Validation

{% 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 %}

Composition API with Reactive Locale

{% 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 %}

Complete Working Example

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 --all

Frontend: 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:

  1. User changes locale in dropdown
  2. Page reloads with ?locale=fr parameter
  3. Laravel middleware detects locale and updates session
  4. Vue components re-render with new translations
  5. Text direction automatically adjusts for RTL languages

Development

# Install dependencies
npm install

# Run tests
npm test

# Build the package
npm run build

# Run linter
npm run lint

# Format code
npm run format

Contributing

Contributions are welcome! Please see CONTRIBUTING for details.

Changelog

Please see CHANGELOG for recent changes.

License

The MIT License (MIT). Please see License File for more information.

Related Packages

Credits

About

Vue 3 integration for Laravel Localizer with Vite plugin, useTranslation composable, and automatic TypeScript generation

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published

Contributors 2

  •  
  •