Internationalization (i18n)
CleanSlice distributes translations across slices. Each slice manages its own locale files in a locales/ folder, and Nuxt's layer system merges them automatically at build time. The setup/i18n slice configures the base module, strategies, and date formatting -- but contains no translations itself.
How It Works
setup/i18n slice
--> Configures @nuxtjs/i18n module
--> Sets default locale, strategy, datetime formats
--> Contains NO translations
Feature slices
--> Each has locales/en.json, locales/fr.json, etc.
--> Each nuxt.config.ts registers its locale files
--> Nuxt layers merge everything at build time
Runtime
--> $t('key') resolves from the merged set
--> $d(date, 'short') formats dates per locale
--> useI18n() for script-level accessInstallation
npm install -D @nuxtjs/i18n@nextBase i18n Slice Configuration
The setup slice configures the i18n module with global settings:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const currentDir = dirname(fileURLToPath(import.meta.url));
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
alias: {
'#i18n': currentDir,
},
i18n: {
// Path to Vue I18n config file
vueI18n: 'i18n.config.ts',
// No locale prefix in URLs (e.g., /about not /en/about)
strategy: 'no_prefix',
// Default language
defaultLocale: 'en',
// Allows HTML and special characters in translations
compilation: {
strictMessage: false,
},
// Detect user's language from browser
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root',
},
},
});Vue I18n Config
This file defines datetime formats and other Vue I18n options:
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {}, // Messages come from slice locale files
datetimeFormats: {
en: {
short: {
year: 'numeric',
month: 'short',
day: 'numeric',
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric',
},
time: {
hour: '2-digit',
minute: 'numeric',
hour12: true,
},
},
fr: {
short: {
year: 'numeric',
month: 'short',
day: 'numeric',
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric',
},
time: {
hour: 'numeric',
minute: 'numeric',
hour12: false,
},
},
},
}));Adding Translations to a Slice
Every slice that needs translated text follows the same pattern:
1. Create a locales folder
slices/user/auth/
├── locales/
│ ├── en.json
│ └── fr.json
└── nuxt.config.ts2. Register in nuxt.config.ts
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const currentDir = dirname(fileURLToPath(import.meta.url));
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
alias: {
'#auth': currentDir,
},
i18n: {
langDir: '../locales',
locales: [
{ code: 'en', file: 'en.json' },
{ code: 'fr', file: 'fr.json' },
],
},
});3. Write the locale files
{
"welcome": "Welcome to the application",
"USER_EXISTS_title": "User Already Registered",
"USER_EXISTS_description": "It looks like you already have an account. Please log in.",
"USER_NOT_AUTHORIZED_title": "Incorrect Email or Password",
"USER_NOT_AUTHORIZED_description": "Please verify your credentials and try again, or contact {supportLink}.",
"USER_BANNED_title": "Account Banned",
"USER_BANNED_description": "Your account has been banned. Please contact {supportLink}."
}{
"locale.en": "English",
"locale.fr": "French",
"Home": "Home",
"Save": "Save",
"Cancel": "Cancel",
"Delete": "Delete",
"UNEXPECTED_ERROR_title": "Oops! Something Went Wrong",
"UNEXPECTED_ERROR_description": "We encountered an unexpected error. Please try again."
}WARNING
Translation keys must be unique across all slices, since Nuxt merges them into a single namespace. Use prefixes to avoid collisions -- for example, auth.welcome instead of welcome if multiple slices might define similar keys.
Using Translations
In Templates
<template>
<div>
<!-- Simple translation -->
<h1>{{ $t('welcome') }}</h1>
<!-- With interpolation -->
<p>{{ $t('USER_BANNED_description', { supportLink: '[email protected]' }) }}</p>
<!-- Date formatting -->
<span>{{ $d(new Date(item.createdAt), 'short') }}</span>
<!-- Alternative date syntax -->
<i18n-d tag="span" :value="new Date(item.createdAt)" format="long" />
</div>
</template>In Scripts and Composables
Use the useI18n composable (auto-imported by Nuxt):
const { t, d, locale } = useI18n();
// Translate a key
const message = t('welcome');
// Format a date
const formattedDate = d(new Date(), 'short');
// Switch locale
locale.value = 'fr';Date Formatting Utility
A reusable utility for formatting dates:
export const formatDate = (date: string) => {
const { d } = useI18n();
return d(new Date(date), 'short');
};Error Code Translation Pattern
CleanSlice uses a consistent convention for translating API error codes. When the API returns an error with a code field (like USER_NOT_FOUND), the error handling system looks up two translation keys:
{CODE}_title-- The error title shown in the toast{CODE}_description-- The error description
{
"USER_NOT_FOUND_title": "User Not Found",
"USER_NOT_FOUND_description": "We couldn't find an account with the provided email."
}The handleError utility resolves these automatically:
API throws: { code: "USER_NOT_FOUND", statusCode: 404 }
--> handleError looks up $t('USER_NOT_FOUND_title')
--> handleError looks up $t('USER_NOT_FOUND_description')
--> Shows toast with translated messageLanguage Switcher Component
Here is a complete language switcher component:
<script setup lang="ts">
const { locale, locales, setLocale } = useI18n();
const availableLocales = computed(() =>
locales.value.filter((i) => i.code !== locale.value)
);
</script>
<template>
<div class="flex gap-2">
<Button
v-for="loc in availableLocales"
:key="loc.code"
variant="ghost"
size="sm"
@click="setLocale(loc.code)"
>
{{ $t(`locale.${loc.code}`) }}
</Button>
</div>
</template>URL Strategy Options
The strategy option in the i18n config controls how the locale appears in URLs:
| Strategy | URL Pattern | Best For |
|---|---|---|
no_prefix | /about | Single-language apps, or language stored in cookie |
prefix | /en/about | All URLs have locale prefix |
prefix_except_default | /about, /fr/about | Default language without prefix |
prefix_and_default | /en/about, /fr/about | All languages always prefixed |
CleanSlice defaults to no_prefix because the language preference is stored in a cookie.
File Organization Summary
slices/
├── setup/
│ └── i18n/
│ ├── nuxt.config.ts # Module config (no translations)
│ └── i18n.config.ts # Datetime formats, fallback locale
├── common/
│ ├── locales/
│ │ ├── en.json # Shared translations
│ │ └── fr.json
│ └── nuxt.config.ts
├── setup/error/
│ ├── locales/
│ │ └── en.json # Error translations
│ └── nuxt.config.ts
└── user/auth/
├── locales/
│ ├── en.json # Auth translations
│ └── fr.json
└── nuxt.config.tsWhat's Next?
- Error Handling -- See how error codes are translated and displayed
- Navigation -- Menu items use i18n keys for their titles
- Slice Structure -- Where locales fit in slice anatomy