Tutorials//10 Min. Lesezeit

Vue i18n im großen Maßstab: Composition API-Muster für mehrsprachige Apps

Eray Gündoğmuş
Teilen

Vue i18n hat sich mit der Composition API erheblich weiterentwickelt. Wer noch den Options API-Mustern aus Tutorials von 2020 folgt, verschenkt viel Potenzial – insbesondere bei Typsicherheit, Lazy Loading und CDN-ausgelieferten Nachrichten.

Dies ist ein praktischer Leitfaden zum Aufbau mehrsprachiger Vue 3-Anwendungen, die skalieren: von komponentenspezifischen Übersetzungen über routenbasiertes Locale-Switching bis hin zur Nuxt 3-Integration. Wir behandeln Muster, die auch im Enterprise-Maßstab funktionieren – nicht nur solche, die für Demos taugen.

vue-i18n mit der Composition API einrichten

Beginnen Sie mit dem Composition API-Modus. Der Options API-Modus existiert noch für die Abwärtskompatibilität, bietet aber nicht die Composable-Muster, die große Codebasen handhabbar machen.

npm install vue-i18n@9
// src/i18n/index.ts
import { createI18n } from 'vue-i18n'

export type MessageSchema = typeof import('./locales/en.json')

export const i18n = createI18n<[MessageSchema], 'en' | 'fr' | 'de'>({
  legacy: false, // Composition API mode
  locale: 'en',
  fallbackLocale: 'en',
  messages: {
    en: {} // loaded lazily
  }
})

Das Setzen von legacy: false ist der entscheidende Schritt. Es schaltet useI18n() und die vollständige Composition API-Oberfläche frei.

useI18n(): Das zentrale Composable

Jede Komponente, die Übersetzungen benötigt, verwendet useI18n(). Das grundlegende Muster:

// src/components/WelcomeHeader.vue
<script setup lang="ts">
import { useI18n } from 'vue-i18n'

const { t, locale } = useI18n()
</script>

<template>
  <h1>{{ t('welcome.heading') }}</h1>
  <p>{{ t('welcome.subheading', { plan: 'Pro' }) }}</p>
</template>

Die Funktion t() ist reaktiv. Wenn locale sich ändert, werden alle t()-Aufrufe automatisch neu ausgewertet. Das bedeutet, dass der Locale-Wechsel ohne manuelles Re-Rendering reibungslos funktioniert.

Globaler vs. lokaler Geltungsbereich

Hier liegen bei der Skalierung die häufigsten Fehler von Teams. vue-i18n unterstützt zwei Geltungsbereiche, und deren sorglose Vermischung erzeugt Wartungsprobleme.

Globaler Geltungsbereich – Nachrichten, die auf Ebene der i18n-Instanz definiert sind und überall zugänglich sind:

// src/i18n/locales/en.json
{
  "nav.home": "Home",
  "nav.pricing": "Pricing",
  "common.save": "Save",
  "common.cancel": "Cancel"
}

Lokaler Geltungsbereich – Nachrichten, die auf eine bestimmte Komponente beschränkt und innerhalb der Komponente definiert sind:

// src/components/BillingForm.vue
<script setup lang="ts">
import { useI18n } from 'vue-i18n'

const { t } = useI18n({
  messages: {
    en: {
      title: 'Billing Information',
      cardNumber: 'Card number',
      expiryDate: 'Expiry date',
      errors: {
        invalidCard: 'Please check your card number'
      }
    },
    fr: {
      title: 'Informations de facturation',
      cardNumber: 'Numéro de carte',
      expiryDate: "Date d'expiration",
      errors: {
        invalidCard: 'Veuillez vérifier votre numéro de carte'
      }
    }
  }
})
</script>

Die Faustregel: Navigation, häufige Aktionen und Fehlermeldungen, die in der gesamten App erscheinen, gehören in den globalen Geltungsbereich. Funktionsspezifische UI-Texte gehören in den lokalen Geltungsbereich, direkt neben der Komponente.

Nachrichten im lokalen Geltungsbereich verschmutzen den globalen Namespace nicht und machen deutlich, welche Übersetzungen zu welcher Funktion gehören. Wenn Sie eine Komponente löschen, löschen Sie damit auch ihre Übersetzungen.

TypeScript-Typsicherheit

Ohne Typsicherheit wird i18n zu einem Laufzeitfehler-Problem. Sie wissen nicht, dass ein Übersetzungsschlüssel fehlt, bis ein Benutzer im Produktivbetrieb eine leere Zeichenkette sieht.

vue-i18n unterstützt vollständige TypeScript-Integration. Das Setup erfordert eine Typdeklaration:

// src/i18n/index.ts
import { createI18n } from 'vue-i18n'
import enMessages from './locales/en.json'

type MessageSchema = typeof enMessages

declare module 'vue-i18n' {
  export interface DefineLocaleMessage extends MessageSchema {}
}

export const i18n = createI18n<false, MessageSchema>({
  legacy: false,
  locale: 'en',
  messages: {
    en: enMessages
  }
})

Jetzt ist t() vollständig typisiert. Übergeben Sie einen nicht vorhandenen Schlüssel, und TypeScript erkennt diesen zur Kompilierzeit:

const { t } = useI18n()

t('nav.home')        // OK
t('nav.homee')       // TypeScript error: Argument of type '"nav.homee"' is not assignable
t('welcome.heading') // OK — if it exists in the schema

Im großen Maßstab entscheidet das darüber, ob i18n eine Wartungslast ist oder unsichtbar bleibt. Ihre IDE vervollständigt Schlüssel automatisch. Tippfehler schlagen zur Build-Zeit fehl, nicht im Produktivbetrieb.

Locales per Lazy Loading laden

Alle Locales beim Start zu laden ist für kleine Apps in Ordnung. Für Enterprise-Apps mit 20+ Sprachen und Tausenden von Schlüsseln ist es das nicht. Die Lösung ist Lazy Loading: Laden Sie das aktive Locale bei Bedarf.

// src/i18n/loader.ts
import { i18n } from './index'

const loadedLocales = new Set<string>(['en']) // en loaded at init

export async function loadLocale(locale: string): Promise<void> {
  if (loadedLocales.has(locale)) return

  const messages = await import(`./locales/${locale}.json`)
  i18n.global.setLocaleMessage(locale, messages.default)
  loadedLocales.add(locale)
}

export async function setLocale(locale: string): Promise<void> {
  await loadLocale(locale)
  i18n.global.locale.value = locale as any
  document.documentElement.setAttribute('lang', locale)
}

Verwenden Sie dies in einer Locale-Wechsel-Komponente:

// src/components/LocaleSwitcher.vue
<script setup lang="ts">
import { setLocale } from '@/i18n/loader'

const locales = [
  { code: 'en', label: 'English' },
  { code: 'fr', label: 'Français' },
  { code: 'de', label: 'Deutsch' },
]

async function handleChange(code: string) {
  await setLocale(code)
}
</script>

<template>
  <select @change="e => handleChange((e.target as HTMLSelectElement).value)">
    <option v-for="l in locales" :key="l.code" :value="l.code">
      {{ l.label }}
    </option>
  </select>
</template>

Jedes Locale-Bundle ist ein separates Chunk. Benutzer laden nur die Sprache herunter, die sie verwenden.

Routenbasiertes Locale-Switching

Für SEO-kritische Apps sollte das Locale in der URL stehen – /en/pricing, /fr/tarifs. Dieser Ansatz gibt jedem Locale seine eigene indexierbare URL und ermöglicht es Suchmaschinen, die Sprachstruktur Ihrer Website zu verstehen.

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { setLocale } from '@/i18n/loader'

const SUPPORTED_LOCALES = ['en', 'fr', 'de']

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/:locale(en|fr|de)',
      component: () => import('@/layouts/LocaleLayout.vue'),
      children: [
        { path: '', component: () => import('@/pages/Home.vue') },
        { path: 'pricing', component: () => import('@/pages/Pricing.vue') },
        { path: 'features', component: () => import('@/pages/Features.vue') },
      ]
    },
    {
      path: '/:pathMatch(.*)*',
      redirect: to => `/en/${to.params.pathMatch}`
    }
  ]
})

router.beforeEach(async (to) => {
  const locale = to.params.locale as string
  if (SUPPORTED_LOCALES.includes(locale)) {
    await setLocale(locale)
  }
})

export default router

Die Locale-Layout-Komponente verwaltet <link rel="alternate" hreflang>-Tags für SEO:

// src/layouts/LocaleLayout.vue
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { useHead } from '@vueuse/head'

const route = useRoute()
const locale = route.params.locale as string
const LOCALES = ['en', 'fr', 'de']

useHead({
  htmlAttrs: { lang: locale },
  link: LOCALES.map(l => ({
    rel: 'alternate',
    hreflang: l,
    href: `https://example.com/${l}${route.path.replace(`/${locale}`, '')}`
  }))
})
</script>

<template>
  <RouterView />
</template>

Pluralisierung und Formatierung

Pluralisierung ist der Punkt, an dem naive i18n-Implementierungen scheitern. Englisch hat zwei Pluralformen. Arabisch hat sechs. Russisch verwendet je nach letzter Ziffer der Zahl unterschiedliche Formen. vue-i18n behandelt das korrekt – vorausgesetzt, Sie verwenden es korrekt.

Pluralisierung:

// en.json
{
  "items": "no items | one item | {count} items"
}

// ru.json — Russian plural rules
{
  "items": "нет элементов | {count} элемент | {count} элемента | {count} элементов"
}
const { t, n, d } = useI18n()

// Pluralization
t('items', 0)   // "no items"
t('items', 1)   // "one item"
t('items', 42)  // "42 items"

// Number formatting with named formats
n(1234567.89, 'currency') // "$1,234,567.89" in en, "1 234 567,89 €" in fr

Definieren Sie benannte Formate auf Ebene der i18n-Instanz:

export const i18n = createI18n({
  legacy: false,
  locale: 'en',
  numberFormats: {
    en: {
      currency: { style: 'currency', currency: 'USD', notation: 'standard' },
      decimal: { style: 'decimal', minimumFractionDigits: 2 }
    },
    fr: {
      currency: { style: 'currency', currency: 'EUR', notation: 'standard' },
      decimal: { style: 'decimal', minimumFractionDigits: 2 }
    }
  },
  datetimeFormats: {
    en: {
      short: { year: 'numeric', month: 'short', day: 'numeric' },
      long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }
    },
    fr: {
      short: { year: 'numeric', month: 'short', day: 'numeric' },
      long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }
    }
  }
})

Jetzt formatieren n() und d() für jedes Locale korrekt, ohne komponentenspezifische Logik.

CDN-ausgelieferte Nachrichten vs. gebündelte JSON-Dateien

JSON-Locale-Dateien in die App zu bündeln hat einen echten Nachteil: Jede Übersetzungsaktualisierung erfordert ein erneutes Deployment. Für Teams, die Übersetzungen häufig aktualisieren – A/B-Tests mit Texten, Fehler korrigieren, für neue Märkte lokalisieren – ist das eine bedeutende Einschränkung.

Die Alternative sind CDN-ausgelieferte Nachrichten. Ihre Übersetzungsdateien liegen auf einem CDN, und Ihre App lädt sie zur Laufzeit. Das funktioniert mit jedem Vue-Setup – Nuxt 3, Vite SPA oder benutzerdefiniertes Server-Rendering.

// src/i18n/cdn-loader.ts
const CDN_BASE = 'https://cdn.example.com/i18n'

export async function loadLocaleFromCDN(locale: string): Promise<Record<string, string>> {
  const res = await fetch(`${CDN_BASE}/${locale}.json`, {
    headers: { 'Cache-Control': 'max-age=3600' }
  })
  if (!res.ok) throw new Error(`Failed to load locale: ${locale}`)
  return res.json()
}

Der Nachteil ist eine Netzwerkanfrage beim Laden des Locales. Mit einem CDN und ordnungsgemäßem Caching ist das nach dem ersten Laden typischerweise ein Cache-Treffer. Der Vorteil ist, dass Ihr Übersetzungsteam Textaktualisierungen sofort ausliefern kann, ohne die Code-Deployment-Pipeline zu berühren.

Bei Better i18n ist die CDN-Auslieferung in die Plattform integriert. Übersetzungen durchlaufen einen Überprüfungsworkflow, werden veröffentlicht und sind sofort global über das CDN verfügbar – kein erneutes Deployment, kein PR für eine Tippfehlerkorrektur. Die Features-Seite erläutert, wie die Auslieferungspipeline funktioniert. Wenn Sie das speziell für ein Vue-Projekt evaluieren, enthält die Entwicklerseite SDK-Integrationsbeispiele.

Nuxt 3-Integration

Nuxt 3 bietet erstklassige i18n-Unterstützung durch @nuxtjs/i18n, das vue-i18n umhüllt und serverseitiges Rendering, Routengenerierung und SEO-Hilfsprogramme hinzufügt.

npm install @nuxtjs/i18n
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],
  i18n: {
    locales: [
      { code: 'en', iso: 'en-US', file: 'en.json' },
      { code: 'fr', iso: 'fr-FR', file: 'fr.json' },
      { code: 'de', iso: 'de-DE', file: 'de.json' }
    ],
    defaultLocale: 'en',
    langDir: 'i18n/locales/',
    lazy: true,
    strategy: 'prefix_except_default',
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: 'i18n_redirected',
      redirectOn: 'root'
    }
  }
})

Mit strategy: 'prefix_except_default' haben englische Routen kein Präfix (/pricing), während andere Locales eines haben (/fr/tarifs). Nuxt verwaltet die Routengenerierung automatisch.

In Komponenten funktioniert useI18n() identisch wie in einer Nicht-Nuxt-App:

// pages/pricing.vue
<script setup lang="ts">
const { t } = useI18n()

useSeoMeta({
  title: t('pricing.meta.title'),
  description: t('pricing.meta.description')
})
</script>

Die SSR-Integration bedeutet, dass übersetzte Inhalte im anfänglichen HTML vorhanden sind – kein Aufflackern von nicht übersetztem Inhalt, und Suchmaschinen sehen die lokalisierte Version.

Den Entwickler-Workflow skalieren

Sobald Ihre Übersetzungsoberfläche wächst, verlagert sich der Engpass vom technischen Setup zum Workflow. Einige Muster, die im großen Maßstab funktionieren:

Halten Sie Übersetzungsschlüssel semantisch, nicht positionell. nav.pricing altert besser als link3. Wenn die Navigation neu gestaltet wird, wird link3 bedeutungslos. nav.pricing ist weiterhin korrekt.

Behandeln Sie fehlende Übersetzungen als Build-Fehler. Konfigurieren Sie Ihren Build so, dass er warnt oder fehlschlägt, wenn ein Schlüssel im Standardlocale vorhanden ist, aber in einem Ziellocale fehlt. Finden Sie es, bevor Benutzer es tun.

Automatisieren Sie die Übersetzungsextraktion. Das Ausführen von vue-i18n-extract oder ähnlichen Tools als Teil des CI stellt sicher, dass jede neue Zeichenkette vor dem Ausliefern für die Übersetzung markiert wird.

Trennen Sie die Übersetzungsüberprüfung von der Code-Überprüfung. Übersetzungsqualität ist eine andere Fähigkeit als Codequalität. Wenn Übersetzer einen PR öffnen müssen, um einen Tippfehler zu korrigieren, haben Sie unnötige Reibung erzeugt. Verwenden Sie eine Übersetzungsplattform, bei der die Sprachüberprüfung in der Sprache stattfindet, nicht in Git.

Die in diesem Artikel beschriebene technische Grundlage behandelt die Engineering-Seite. Die Workflow-Seite – Übersetzungen überprüfen lassen, Übersetzerzugang verwalten, Glossare sprachübergreifend konsistent halten – ist der Bereich, in dem dedizierte Tools unverhältnismäßig großen Nutzen bringen.

Comments

Loading comments...