Implementing i18n in Vue 3 + Vite Project
Summary
This article uses a real Vue 3 + Vite + TypeScript project as an example to show step-by-step how to access 'vue-i18n@9', implement lazy loading of language packs, use translation in components and routes, synchronize language state with Pinia, and give verification and release precautions. The sample code is located in the project files: 'src/i18n.ts', 'src/components/common/ub_language_switcher.vue', etc.
Target Audience
Front-end engineers, Vue3 users, teams looking to take internationalization from PoC to production.
Project and premise
- Tech stack: Vue 3 + Vite + TypeScript
- Status Management: Pinia ('src/stores/index.ts')
- UI: Arco Design (globally registered at 'src/main.ts')
- Key Documents (In-Project):
- 'src/i18n.ts' — i18n initialization and lazy loading implementation
- 'src/locales/zh.ts' / 'src/locales/en.ts' — language pack
- 'src/components/common/ub_language_switcher.vue' — Language switching component example
- 'src/main.ts' — Loads the language before mounting
- 'src/stores/index.ts' — Optional language state sync point
- 'src/router/index.ts' — example of internationalization of route titles
1. Installation
Run at the project root:
bash
npm install vue-i18n@9
2. Initialization (Core Idea)
Use the Composition API pattern to implement 'loadLocale(lang)' to lazily load language packages. Key takeaways:
- The initial message is empty
- Dynamically inject language content using 'i18n.global.setLocaleMessage'
- Save the user's selected language in 'localStorage' to avoid losing it after refreshing
Key code (from 'src/i18n.ts'):
ts
import { createI18n } from 'vue-i18n'
const DEFAULT_LANG = localStorage.getItem('lang') || 'zh'
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: DEFAULT_LANG,
fallbackLocale: 'en',
messages: {}
})
export async function loadLocale(lang: string) {
if (i18n.global.availableLocales.includes(lang)) {
i18n.global.locale.value = lang
localStorage.setItem('lang', lang)
return
}
const msgs = await import(/* @vite-ignore */ `./locales/${lang}.ts`)
i18n.global.setLocaleMessage(lang, msgs.default || msgs)
i18n.global.locale.value = lang
localStorage.setItem('lang', lang)
}
export default i18n
3. Register at the entrance and preload the language
Register the i18n in 'src/main.ts' and load the default language before mounting to avoid flickering above the fold:
ts
import i18n, { loadLocale } from './i18n'
app.use(i18n)
const lang = localStorage.getItem('lang') || 'zh'
loadLocale(lang).finally(() => app.mount('#app'))
4. Component layer switching (UI example)
Example component in the project: 'src/components/common/ub_language_switcher.vue', use the Arco icon (or button) to click to switch language, call 'loadLocale' and write 'localStorage'. Example highlights:
- Call 'loadLocale(lang)' on switchover
- Optional synchronization Pinia: 'useStore().setLanguage(lang)' (if implemented)
- 'useI18n()' can be used to listen for 'locale'
Example snippet (excerpt):
vue
<template>
<div class="ub-language-switcher" @click="toggleLanguage">
<IconChineseFill v-if="current === 'zh'"/>
<IconEnglishFill v-else />
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
Call loadLocale(...) on switchover
</script>
5. Lazy loading vs. build impact
- Lazy loading language packs will chunk each language as a separate chunk (Vite's dynamic import behavior).
- Advantages: small size of the first pack; Disadvantage: Build more output language packages.
- Verification: You can see the language chunk request by switching the language in the Network panel of the browser's DevTools.
6. Pinia Sync (optional)
Add the 'language' state and the 'setLanguage' action to 'src/stores/index.ts' to make the language a global state:
ts
// state:
language: localStorage.getItem('lang') || 'zh',
// action:
async setLanguage(lang: string) {
this.language = lang
localStorage.setItem('lang', lang)
await import('@/i18n').then(m => m.loadLocale(lang))
}
7. Route title internationalization
Change the route 'meta.title' to 'meta.titleKey' and set 'document.title' after the switch:
ts
import { tGlobal } from '@/i18n'
router.afterEach((to) => {
const key = (to.meta as any).titleKey
if (key) document.title = tGlobal(key as string) as string
})
8. Date and number localization
- 'dayjs' or Intl API is recommended.
- If you are using dayjs, you can dynamically load dayjs locale and call dayjs.locale(lang)' after 'loadLocale' is successful.
9. Verification steps
bash
npm install
npm run dev
Verification Points:
- 'lang' in localStorage is correct ('zh' or 'en')
- Page text changes after switching languages
- Lazy loading language chunks appear in the Network panel
- If enabled, the browser tab title changes with the route key
10. FAQs and debugging
- Not Effective: Confirm that 'i18n' is registered with 'main.ts' and load the language before mounting
- Lazy loading failed: Check the language package path 'src/locales/{lang}.ts'
- Asynchronous request requires locale header: read 'i18n.global.locale.value' on request
11. SEO, accessibility, and publishing recommendations
- Populate the title/description with 'meta.titleKey' and the corresponding translation (if you need SSR, you need to handle it on the server side)
- Add 'aria-label' or 'a-tooltip' to toggle controls to improve accessibility
- Published with 2~3 screenshots: language switching, network lazy loading, route title change
12. Conclusion and expansion
By lazily loading language packs and synchronizing language states with Pinia, internationalization can be both lightweight and maintainable. The next step is to consider machine translation first draft + manual verification, or connect to a translation management platform (such as Crowdin, POEditor).