Initial commit
This commit is contained in:
757
skills/nuxt/references/nuxt-i18n.md
Normal file
757
skills/nuxt/references/nuxt-i18n.md
Normal file
@@ -0,0 +1,757 @@
|
||||
# Nuxt I18n Reference
|
||||
|
||||
**Last Updated:** 2025-11 (v10.2.0)
|
||||
|
||||
**Check:** `@nuxtjs/i18n` in package.json
|
||||
|
||||
Nuxt I18n v10 provides internationalization (i18n) for Nuxt applications with auto-imports, locale routing, SEO support, and integration with Vue I18n v11.
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
```bash
|
||||
pnpm add @nuxtjs/i18n
|
||||
```
|
||||
|
||||
**nuxt.config.ts:**
|
||||
|
||||
```typescript
|
||||
export default defineNuxtConfig({
|
||||
modules: ["@nuxtjs/i18n"],
|
||||
|
||||
i18n: {
|
||||
locales: [
|
||||
{ code: "en", iso: "en-US", name: "English", file: "en.json" },
|
||||
{ code: "fr", iso: "fr-FR", name: "Français", file: "fr.json" },
|
||||
{ code: "es", iso: "es-ES", name: "Español", file: "es.json" },
|
||||
],
|
||||
defaultLocale: "en",
|
||||
langDir: "locales/",
|
||||
strategy: "prefix_except_default", // or 'prefix', 'no_prefix'
|
||||
detectBrowserLanguage: {
|
||||
useCookie: true,
|
||||
cookieKey: "i18n_redirected",
|
||||
redirectOn: "root",
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
locales/
|
||||
├── en.json
|
||||
├── fr.json
|
||||
└── es.json
|
||||
```
|
||||
|
||||
**Example locale file (en.json):**
|
||||
|
||||
```json
|
||||
{
|
||||
"welcome": "Welcome",
|
||||
"hello": "Hello {name}",
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"contact": "Contact"
|
||||
},
|
||||
"products": {
|
||||
"title": "Our Products",
|
||||
"description": "Browse our collection"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Translation in Templates
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- Simple translation -->
|
||||
<h1>{{ $t("welcome") }}</h1>
|
||||
|
||||
<!-- With parameters -->
|
||||
<p>{{ $t("hello", { name: "John" }) }}</p>
|
||||
|
||||
<!-- Nested keys -->
|
||||
<nav>
|
||||
<NuxtLink to="/">{{ $t("nav.home") }}</NuxtLink>
|
||||
<NuxtLink to="/about">{{ $t("nav.about") }}</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<!-- Pluralization -->
|
||||
<p>{{ $t("items", { count: 5 }) }}</p>
|
||||
|
||||
<!-- Number formatting -->
|
||||
<p>{{ $n(1000, "currency") }}</p>
|
||||
|
||||
<!-- Date formatting -->
|
||||
<p>{{ $d(new Date(), "long") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Translation in Script
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { t, locale, locales, setLocale } = useI18n()
|
||||
|
||||
// Get translation
|
||||
const welcomeMessage = t("welcome")
|
||||
|
||||
// With parameters
|
||||
const greeting = t("hello", { name: "John" })
|
||||
|
||||
// Current locale
|
||||
console.log(locale.value) // 'en'
|
||||
|
||||
// Available locales
|
||||
console.log(locales.value) // [{ code: 'en', ... }, ...]
|
||||
|
||||
// Change locale
|
||||
async function switchLanguage(code: string) {
|
||||
await setLocale(code)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
### Route Strategies
|
||||
|
||||
**prefix_except_default** (recommended):
|
||||
|
||||
- Default locale: `/about`
|
||||
- Other locales: `/fr/about`, `/es/about`
|
||||
|
||||
**prefix**:
|
||||
|
||||
- All locales: `/en/about`, `/fr/about`, `/es/about`
|
||||
|
||||
**no_prefix**:
|
||||
|
||||
- All locales use same path: `/about`
|
||||
- Locale detected from cookie/browser
|
||||
|
||||
### Locale Links
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const localePath = useLocalePath()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Link with current locale -->
|
||||
<NuxtLink :to="localePath('/')">
|
||||
{{ $t("nav.home") }}
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink :to="localePath('/about')">
|
||||
{{ $t("nav.about") }}
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Switch locale links -->
|
||||
<NuxtLink :to="switchLocalePath('fr')"> Français </NuxtLink>
|
||||
|
||||
<NuxtLink :to="switchLocalePath('es')"> Español </NuxtLink>
|
||||
|
||||
<!-- With named routes -->
|
||||
<NuxtLink :to="localePath({ name: 'products-id', params: { id: '123' } })">
|
||||
Product
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const localePath = useLocalePath()
|
||||
const router = useRouter()
|
||||
|
||||
async function goToAbout() {
|
||||
await router.push(localePath("/about"))
|
||||
}
|
||||
|
||||
async function goToProduct(id: string) {
|
||||
await navigateTo(localePath({ name: "products-id", params: { id } }))
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Composables
|
||||
|
||||
### useI18n
|
||||
|
||||
Main composable for translations:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
t, // Translation function
|
||||
locale, // Current locale ref
|
||||
locales, // Available locales
|
||||
setLocale, // Change locale function
|
||||
n, // Number formatting
|
||||
d, // Date formatting
|
||||
tm, // Translation messages
|
||||
te, // Translation exists check
|
||||
getLocaleCookie, // Get locale cookie
|
||||
} = useI18n()
|
||||
|
||||
// Check if translation exists
|
||||
if (te("optional.message")) {
|
||||
console.log(t("optional.message"))
|
||||
}
|
||||
|
||||
// Get all translations for a key
|
||||
const navTranslations = tm("nav")
|
||||
console.log(navTranslations) // { home: 'Home', about: 'About', ... }
|
||||
</script>
|
||||
```
|
||||
|
||||
### useLocalePath
|
||||
|
||||
Generate localized paths:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const localePath = useLocalePath()
|
||||
|
||||
// Simple path
|
||||
const homePath = localePath("/")
|
||||
|
||||
// With route object
|
||||
const productPath = localePath({
|
||||
name: "products-id",
|
||||
params: { id: "123" },
|
||||
query: { tab: "reviews" },
|
||||
})
|
||||
|
||||
// With specific locale
|
||||
const frenchPath = localePath("/about", "fr")
|
||||
</script>
|
||||
```
|
||||
|
||||
### useSwitchLocalePath
|
||||
|
||||
Generate paths for locale switching:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
const route = useRoute()
|
||||
|
||||
// Get path for switching to French on current page
|
||||
const frenchPath = switchLocalePath("fr")
|
||||
|
||||
// Custom route
|
||||
const customPath = switchLocalePath("es", "/about")
|
||||
</script>
|
||||
```
|
||||
|
||||
### useRouteBaseName
|
||||
|
||||
Get route name without locale prefix:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const getRouteBaseName = useRouteBaseName()
|
||||
const route = useRoute()
|
||||
|
||||
// Current route: /fr/products/123
|
||||
const baseName = getRouteBaseName(route) // 'products-id'
|
||||
</script>
|
||||
```
|
||||
|
||||
### useBrowserLocale
|
||||
|
||||
Detect browser locale:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const browserLocale = useBrowserLocale()
|
||||
console.log(browserLocale) // 'en-US'
|
||||
</script>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Language Switcher
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { locale, locales, setLocale } = useI18n()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
const availableLocales = computed(() =>
|
||||
locales.value.filter((l) => l.code !== locale.value),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
v-for="loc of availableLocales"
|
||||
:key="loc.code"
|
||||
:to="switchLocalePath(loc.code)"
|
||||
class="px-3 py-1 rounded hover:bg-gray-100"
|
||||
>
|
||||
{{ loc.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Dropdown Language Switcher
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { locale, locales } = useI18n()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
|
||||
const currentLocale = computed(() =>
|
||||
locales.value.find((l) => l.code === locale.value),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDropdown>
|
||||
<template #trigger>
|
||||
<UButton>
|
||||
{{ currentLocale?.name }}
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
<div class="p-2">
|
||||
<NuxtLink
|
||||
v-for="loc of locales"
|
||||
:key="loc.code"
|
||||
:to="switchLocalePath(loc.code)"
|
||||
class="block px-4 py-2 hover:bg-gray-100"
|
||||
>
|
||||
{{ loc.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</UDropdown>
|
||||
</template>
|
||||
```
|
||||
|
||||
### SEO with I18n
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { locale, locales, t } = useI18n()
|
||||
const route = useRoute()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
|
||||
useSeoMeta({
|
||||
title: t("seo.title"),
|
||||
description: t("seo.description"),
|
||||
ogLocale: locale.value,
|
||||
ogTitle: t("seo.ogTitle"),
|
||||
ogDescription: t("seo.ogDescription"),
|
||||
})
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: locale.value,
|
||||
},
|
||||
link: [
|
||||
// Alternate language links for SEO
|
||||
...locales.value.map((loc) => ({
|
||||
rel: "alternate",
|
||||
hreflang: loc.iso,
|
||||
href: `https://example.com${switchLocalePath(loc.code)}`,
|
||||
})),
|
||||
],
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Per-Page Translations
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n({
|
||||
useScope: "local",
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ t("title") }}</h1>
|
||||
<p>{{ t("description") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n lang="json">
|
||||
{
|
||||
"en": {
|
||||
"title": "About Us",
|
||||
"description": "Learn more about our company"
|
||||
},
|
||||
"fr": {
|
||||
"title": "À propos",
|
||||
"description": "En savoir plus sur notre entreprise"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
```
|
||||
|
||||
### Dynamic Content Translation
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data: products } = await useFetch("/api/products")
|
||||
const { locale } = useI18n()
|
||||
|
||||
// Assuming API returns translations
|
||||
const localizedProducts = computed(() =>
|
||||
products.value?.map((product) => ({
|
||||
...product,
|
||||
name: product.translations[locale.value]?.name || product.name,
|
||||
description:
|
||||
product.translations[locale.value]?.description || product.description,
|
||||
})),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="product of localizedProducts" :key="product.id">
|
||||
<h3>{{ product.name }}</h3>
|
||||
<p>{{ product.description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Lazy Loading Translations
|
||||
|
||||
For large translation files:
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
i18n: {
|
||||
lazy: true,
|
||||
langDir: "locales/",
|
||||
locales: [
|
||||
{ code: "en", file: "en.json" },
|
||||
{ code: "fr", file: "fr.json" },
|
||||
{ code: "es", file: "es.json" },
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Number & Date Formatting
|
||||
|
||||
### Number Formatting
|
||||
|
||||
Define formats in config:
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
i18n: {
|
||||
numberFormats: {
|
||||
en: {
|
||||
currency: {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
},
|
||||
decimal: {
|
||||
style: "decimal",
|
||||
minimumFractionDigits: 2,
|
||||
},
|
||||
},
|
||||
fr: {
|
||||
currency: {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
},
|
||||
decimal: {
|
||||
style: "decimal",
|
||||
minimumFractionDigits: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- Currency -->
|
||||
<p>{{ $n(1234.56, "currency") }}</p>
|
||||
<!-- en: $1,234.56 -->
|
||||
<!-- fr: 1 234,56 € -->
|
||||
|
||||
<!-- Decimal -->
|
||||
<p>{{ $n(123456.789, "decimal") }}</p>
|
||||
<!-- en: 123,456.79 -->
|
||||
<!-- fr: 123 456,79 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Date Formatting
|
||||
|
||||
Define formats in config:
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
i18n: {
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const date = new Date("2025-01-04")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Short format -->
|
||||
<p>{{ $d(date, "short") }}</p>
|
||||
<!-- en: Jan 4, 2025 -->
|
||||
<!-- fr: 4 janv. 2025 -->
|
||||
|
||||
<!-- Long format -->
|
||||
<p>{{ $d(date, "long") }}</p>
|
||||
<!-- en: Saturday, January 4, 2025 -->
|
||||
<!-- fr: samedi 4 janvier 2025 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Pluralization
|
||||
|
||||
**Translation file:**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": "no items | one item | {count} items",
|
||||
"cart": "You have {n} item in your cart | You have {n} items in your cart"
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<p>{{ $t("items", 0) }}</p>
|
||||
<!-- "no items" -->
|
||||
<p>{{ $t("items", 1) }}</p>
|
||||
<!-- "one item" -->
|
||||
<p>{{ $t("items", 5) }}</p>
|
||||
<!-- "5 items" -->
|
||||
|
||||
<p>{{ $t("cart", { n: 1 }) }}</p>
|
||||
<!-- "You have 1 item in your cart" -->
|
||||
<p>{{ $t("cart", { n: 3 }) }}</p>
|
||||
<!-- "You have 3 items in your cart" -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
### Typed Translations
|
||||
|
||||
```typescript
|
||||
// types/i18n.ts
|
||||
export interface LocaleMessages {
|
||||
welcome: string
|
||||
hello: (params: { name: string }) => string
|
||||
nav: {
|
||||
home: string
|
||||
about: string
|
||||
contact: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { LocaleMessages } from "~/types/i18n"
|
||||
|
||||
const { t } = useI18n<LocaleMessages>()
|
||||
|
||||
// TypeScript will check keys and parameters
|
||||
const welcome = t("welcome")
|
||||
const hello = t("hello", { name: "John" })
|
||||
const navHome = t("nav.home")
|
||||
</script>
|
||||
```
|
||||
|
||||
## API Routes with I18n
|
||||
|
||||
```typescript
|
||||
// server/api/products.get.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const locale = getCookie(event, "i18n_redirected") || "en"
|
||||
|
||||
const products = await db.products.findMany({
|
||||
include: {
|
||||
translations: {
|
||||
where: { locale },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return products.map((product) => ({
|
||||
...product,
|
||||
name: product.translations[0]?.name || product.name,
|
||||
description: product.translations[0]?.description || product.description,
|
||||
}))
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use prefix_except_default strategy** - Better UX for default locale users
|
||||
2. **Enable browser detection** - Auto-redirect to user's preferred language
|
||||
3. **Provide language switcher** - Always visible on every page
|
||||
4. **Use lazy loading for large apps** - Load translations on demand
|
||||
5. **Organize translations by feature** - Use nested keys (`nav.home`, `products.title`)
|
||||
6. **Include locale in SEO** - Use `hreflang` links and `og:locale`
|
||||
7. **Handle missing translations** - Provide fallback locale
|
||||
8. **Use placeholders consistently** - `{name}` for simple, `{n}` for pluralization
|
||||
9. **Test all locales** - Verify layout with longer translations (German, French)
|
||||
10. **Keep keys consistent** - Same structure across all locale files
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Translations Not Loading
|
||||
|
||||
Check:
|
||||
|
||||
1. `@nuxtjs/i18n` in `modules` array
|
||||
2. Locale files in correct `langDir` path
|
||||
3. File names match `file` property in config
|
||||
4. JSON is valid (no trailing commas)
|
||||
|
||||
### Routes Not Localized
|
||||
|
||||
1. Verify `strategy` is set correctly
|
||||
2. Check `defaultLocale` matches one of your locales
|
||||
3. Ensure you're using `localePath()` for links
|
||||
|
||||
### Browser Detection Not Working
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
i18n: {
|
||||
detectBrowserLanguage: {
|
||||
useCookie: true,
|
||||
cookieKey: "i18n_redirected",
|
||||
redirectOn: "root",
|
||||
alwaysRedirect: true,
|
||||
fallbackLocale: "en",
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Missing Translations
|
||||
|
||||
Enable warnings in development:
|
||||
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
i18n: {
|
||||
compilation: {
|
||||
strictMessage: false,
|
||||
},
|
||||
vueI18n: "./i18n.config.ts",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```typescript
|
||||
// i18n.config.ts
|
||||
export default {
|
||||
legacy: false,
|
||||
locale: "en",
|
||||
missingWarn: true,
|
||||
fallbackWarn: true,
|
||||
}
|
||||
```
|
||||
|
||||
## v10 Changes from v9
|
||||
|
||||
### Key Updates
|
||||
|
||||
1. **Vue I18n v11** - Upgraded from v10 with JIT compilation as default
|
||||
2. **Improved Nuxt 4 Support** - Better compatibility with Nuxt 4
|
||||
3. **Custom Routes** - Use `definePageMeta` for per-page locale configuration
|
||||
4. **Server-Side Redirects** - Improved server-side redirection behavior
|
||||
5. **Strict SEO** - Experimental strict SEO head management
|
||||
6. **Fixed Behaviors** - `strategy` and `redirectOn` combinations now work as expected
|
||||
|
||||
### Migration Notes
|
||||
|
||||
- `$tc()` API integrated into `$t()` (from Vue I18n v10→v11 upgrade)
|
||||
- JIT compilation now default (no need for `jit` option)
|
||||
- New directory structure: i18n files resolved from `<rootDir>/i18n` (configurable with `restructureDir`)
|
||||
- Context functions require `$` prefix: use `$localePath()` not `localePath()` in templates
|
||||
|
||||
## Official Resources
|
||||
|
||||
- **Documentation:** https://i18n.nuxtjs.org
|
||||
- **Migration Guide:** https://i18n.nuxtjs.org/docs/guide/migrating
|
||||
- **API Reference:** https://i18n.nuxtjs.org/api
|
||||
- **Vue I18n Docs:** https://vue-i18n.intlify.dev
|
||||
- **Examples:** https://i18n.nuxtjs.org/examples
|
||||
- **GitHub:** https://github.com/nuxt-modules/i18n
|
||||
|
||||
---
|
||||
|
||||
**Note:** This reference covers Nuxt I18n v10 (latest as of 2025-11) with Vue I18n v11. For v9 projects, consult the migration guide.
|
||||
Reference in New Issue
Block a user