Multi-language support
Setting up
- Install the Internationalization plugin for Vue
bash
pnpm install vue-i18n
pnpm install @intlify/unplugin-vue-i18n- Add plugin to
./vite.config.ts
ts
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import path from 'node:path'
// ...
export default defineConfig({
plugins: [
// ... other plugins
VueI18nPlugin({
include: path.resolve(__dirname, './src/i18n/**')
}),
],
})- Create some initial localization files.
@/i18n/en.json - / EN file
json
{
"home": "Home"
}@/i18n/fr.json - / FR file
json
{
"home": "Accueil"
}@/i18n/sandbox/en.json - /sandbox EN file
json
{
"sandbox.title": "Sandbox",
"sandbox.description": "A sandbox for testing i18n features in a Vue application."
}@/i18n/sandbox/fr.json - /sandbox FR file
json
{
"sandbox.title": "Sandbox",
"sandbox.description": "Un bac à sable pour tester les fonctionnalités i18n dans une application Vue."
}Translation File Structure:
src/i18n/
├── en.json # Base English translations
├── fr.json # Base French translations
└── page-a/ # Page-specific translations
├── en.json
└── fr.jsonNOTE
Translation structure can be only one level deep, so everything under moduleA has to be included in either base or module translation files.
- Create i18n configuration
@/plugins/i18n.ts
ts
import { createI18n } from 'vue-i18n'
import messages from '@intlify/unplugin-vue-i18n/messages'
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: 'en',
fallbackLocale: 'en',
fallbackWarn: false,
messages,
})
export default i18n- Initialize i18n in main.ts
ts
// ...
import i18n from './plugins/i18n'
// ...
app.use(i18n)
// ...- Test
Modify @/App.vue
vue
<template>
<router-link to="/">Home</router-link>
|
<router-link to="/about">About</router-link>
|
<router-link to="/sandbox">Sandbox</router-link>
<br />
<br />
<v-radio-group inline v-model="locale" :label="t('choose.language')">
<v-radio label="en" value="en"></v-radio>
<v-radio label="fr" value="fr"></v-radio>
</v-radio-group>
<br />
<br />
<router-view />
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { locale, t } = useI18n()
</script>And @/pages/sandbox/index.vue
vue
<template>
<v-card :style="cardBackground">
<v-card-title><v-icon icon="$mdiHome" />{{ t('sandbox.title') }}</v-card-title>
<v-card-text>{{ t('sandbox.description') }}</v-card-text>
<v-card-actions>
<v-btn @click="console.log('Primary!')" color="primary">Primary</v-btn>
<v-btn @click="console.log('Secondary!')" color="secondary">Secondary</v-btn>
<v-btn @click="theme.toggle()">Toggle theme</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { useTheme } from 'vuetify'
import { useCardBackground } from '@/composables/ui'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const theme = useTheme()
const cardBackground = useCardBackground('#0000ff')
</script>Automation (Optional)
NOTE
This feature is experimental
Translation automation will involve following steps:
Gathering of missing translations - using i18n Missing Handler
Passing and caching missing translations to vite middleware handler.
Processing cached missed translations in timed intervals and adding to translation files.
Files can be then AI translated.
Implementation
- Create Vite Dev plugin for processing translations
@/plugins/i18n-dev.ts
source
ts
import { promises as fs, existsSync } from 'node:fs'
import path from 'node:path'
import type { Plugin, ViteDevServer } from 'vite'
const i18nLocales = ['en', 'fr', 'de']
const i18nRefreshInterval = 300000 // 300 seconds by default
type I18nCache = {
module?: string
locale: string
key: string
value: string
}
// Determine the correct i18n path for a module
// If module exists in src/modules/{module}/, use src/modules/{module}/i18n/
// Otherwise use the legacy src/i18n/{module}/ path
function getI18nPath(module: string | undefined, locale: string): string {
if (module) {
const modulePath = path.resolve(process.cwd(), 'src', 'modules', module)
if (existsSync(modulePath)) {
return path.resolve(modulePath, 'i18n', `${locale}.json`)
}
}
// Legacy path or root translations
return path.resolve(process.cwd(), 'src', 'i18n', module || '', `${locale}.json`)
}
export function i18nDevPlugin(): Plugin {
const i18nCache: I18nCache[] = []
let timer: NodeJS.Timeout | null = null
async function dumpI18nData() {
if (i18nCache.length === 0) return
try {
const modules = [...[''], ...Array.from(new Set(i18nCache.map((item) => item.module)))]
const locales = i18nLocales
const fileGroups = new Map<string, I18nCache[]>()
for (const module of modules) {
for (const locale of locales) {
const filePath = getI18nPath(module, locale)
if (!fileGroups.has(filePath)) {
fileGroups.set(filePath, [])
}
const items = i18nCache.filter((item) => item.module === module && item.locale === locale)
for (const item of items) {
fileGroups.get(filePath)!.push(item)
}
}
}
for (const [filePath, items] of fileGroups.entries()) {
const fileExists = await fs
.stat(filePath)
.then(() => true)
.catch(() => false)
if (!fileExists) {
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, '{}')
}
const data = await fs.readFile(filePath, 'utf-8')
const jsonData = JSON.parse(data || '{}')
let hasChanges = false
for (const item of items) {
if (item.key && !jsonData[item.key]) {
jsonData[item.key] = item.value
hasChanges = true
}
}
if (hasChanges) {
await fs.writeFile(filePath, JSON.stringify(jsonData, null, 2))
const displayPath = path.relative(process.cwd(), filePath)
console.log(`🌐 [i18n] Updated file: ${displayPath} with ${items.length} translations`)
}
}
i18nCache.length = 0
} catch (error) {
console.error('❌ Error processing i18n dump:', error)
}
}
return {
name: 'i18n-dev-plugin',
configureServer(server: ViteDevServer) {
server.middlewares.use('/i18n-add', (req, res) => {
if (req.method === 'POST') {
let body = ''
req.on('data', (chunk) => {
body += chunk
})
req.on('end', () => {
try {
const data = JSON.parse(body)
const referer = req.headers.referer || 'unknown'
const module = referer.split('/')[3] || ''
const item = i18nCache.find(
(item) =>
item.module === module &&
item.key === data.data.key &&
item.locale === data.data.locale,
)
if (!item) {
i18nCache.push({
module,
locale: data.data.locale,
key: data.data.key,
value: data.data.key
.split('.')
.join(' ')
.replace(/^./, (c: string) => c.toUpperCase()),
})
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ status: 'ok', received: data }))
} catch (err) {
console.error('❌ Failed to parse JSON:', err)
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Invalid JSON' }))
}
})
} else {
res.writeHead(405, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Method Not Allowed' }))
}
})
server.middlewares.use('/i18n-status', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ status: 'ok', cache: i18nCache }))
})
timer = setInterval(() => {
dumpI18nData().catch((error) => {
console.error('❌ Error in timed i18n dump:', error)
})
}, i18nRefreshInterval)
server.httpServer?.on('close', () => {
if (timer) {
clearInterval(timer)
timer = null
}
})
},
}
}- Add plugin to
./vite.config.tsamd enable middleware
ts
//
import { i18nDevPlugin} from './src/plugins/i18n-dev'
//
return {
middlewareMode: true,
plugins: [
//
i18nDevPlugin(),
//
],
}
//- Add missing translated handler to
@/plugins/i18n.ts
ts
/// <reference types="vite/client" />
import { createI18n } from 'vue-i18n'
import messages from '@intlify/unplugin-vue-i18n/messages'
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: 'en',
fallbackLocale: 'en',
fallbackWarn: false,
messages,
missing: (locale: string, key: string) => {
if (!import.meta.env.DEV) return
if (typeof window !== 'undefined' && window.location.href) {
fetch('/i18n-add', {
method: 'POST',
body: JSON.stringify({
data: { locale, key },
}),
headers: {
'Content-Type': 'application/json',
},
}).catch(() => {
// Silently fail if fetch fails (e.g., in test environment)
})
}
},
})
export default i18n- Test.
Add in any *.vue file
vue
<p>{{ t('welcome.message') }}</p>And When navigating through pages in development mode, each 30 seconds there should be update of files in ./i18n with missing translations.
log
2:44:51 PM [vite] (client) page reload src/i18n/sandbox/en.json
🌐 [i18n] Updated file: src\i18n\sandbox\en.json with 1 translations
2:44:51 PM [vite] (client) page reload src/i18n/sandbox/fr.json
🌐 [i18n] Updated file: src\i18n\sandbox\fr.json with 1 translations