feat: 20250611 配置 @intlify/unplugin-vue-i18n,国际化I18n多语言

This commit is contained in:
mac·ufutx 2025-06-11 20:15:22 +08:00
parent 420fd4f3ff
commit 481ec5ecf7
12 changed files with 356 additions and 3 deletions

View File

@ -25,11 +25,13 @@
"pinia": "^2.1.7",
"postcss-px-to-viewport": "^1.1.1",
"vue": "^3.5.13",
"vue-i18n": "^9.8.0",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@types/node": "^20.19.0",
"@typescript-eslint/eslint-plugin": "6.13.2",
"@typescript-eslint/parser": "6.13.2",

View File

@ -0,0 +1,20 @@
{
"button": {
"submit": "Submit",
"cancel": "Cancel",
"back": "Back",
"next": "Next"
},
"message": {
"welcome": "Welcome to our application",
"loading": "Loading...",
"success": "Operation successful",
"error": "An error occurred"
},
"navigation": {
"home": "Home",
"about": "About",
"contact": "Contact",
"profile": "Profile"
}
}

18
src/locales/en/home.json Normal file
View File

@ -0,0 +1,18 @@
{
"title": "Home Page",
"subtitle": "Welcome to our website",
"hero": {
"heading": "Discover our products",
"description": "Explore our range of high-quality solutions",
"cta": "Learn More"
},
"features": {
"title": "Key Features",
"items": [
"Fast Performance",
"Easy Integration",
"Secure Transactions",
"24/7 Support"
]
}
}

View File

@ -0,0 +1,28 @@
{
"page": {
"title": "Internationalization Demo"
},
"demo": {
"date": {
"title": "Date Formatting",
"current": "Current Date"
},
"number": {
"title": "Number Formatting",
"price": "Product Price"
},
"plural": {
"title": "Pluralization",
"message": "{count, plural, zero {No messages} one {One message} other {# messages}}"
},
"nested": {
"title": "Nested Translation",
"content": "This is a nested translation example with {placeholder}."
},
"router": {
"title": "Router Links",
"home": "Home",
"about": "About"
}
}
}

61
src/locales/i18n.ts Normal file
View File

@ -0,0 +1,61 @@
// src/locales/i18n.ts
import { createI18n } from 'vue-i18n'
import en from './en/common.json'
console.log('Manual import:', en)
// 关键:正确加载语言文件(假设语言文件直接放在 locales 目录下)
// src/locales/i18n.ts
const loadLocaleMessages = () => {
const messages: Record<string, any> = {}
const localeFiles = import.meta.glob('./**/*.json', { eager: true })
for (const path in localeFiles) {
const pathParts = path.split('/').slice(1)
const locale = pathParts[0] // 'en' 或 'zh-CN'
const module = pathParts[1].replace('.json', '') // 'i18nDemo'
if (!messages[locale]) {
messages[locale] = {}
}
messages[locale][module] = localeFiles[path]
}
// 合并模块到语言层级(而非顶层)
const mergedMessages: Record<string, any> = {}
for (const locale in messages) {
mergedMessages[locale] = {}
for (const module in messages[locale]) {
Object.assign(mergedMessages[locale], messages[locale][module])
}
}
console.log('Final messages:', mergedMessages)
return mergedMessages
}
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
fallbackLocale: 'en',
messages: loadLocaleMessages(),
datetimeFormats: {
en: {
short: { year: 'numeric', month: 'short', day: 'numeric' }
},
'zh-CN': {
short: { year: 'numeric', month: '2-digit', day: '2-digit' }
}
},
numberFormats: {
en: {
currency: { style: 'currency', currency: 'USD' }
},
'zh-CN': {
currency: { style: 'currency', currency: 'CNY' }
}
}
}) as any // 暂时忽略类型检查
export default i18n

View File

@ -0,0 +1,20 @@
{
"button": {
"submit": "提交",
"cancel": "取消",
"back": "返回",
"next": "下一步"
},
"message": {
"welcome": "欢迎使用我们的应用",
"loading": "加载中...",
"success": "操作成功",
"error": "发生错误"
},
"navigation": {
"home": "首页",
"about": "关于我们",
"contact": "联系我们",
"profile": "个人资料"
}
}

View File

@ -0,0 +1,18 @@
{
"title": "首页",
"subtitle": "欢迎访问我们的网站",
"hero": {
"heading": "发现我们的产品",
"description": "探索我们的高品质解决方案系列",
"cta": "了解更多"
},
"features": {
"title": "核心功能",
"items": [
"快速性能",
"简单集成",
"安全交易",
"全天候支持"
]
}
}

View File

@ -0,0 +1,29 @@
{
"page": {
"title": "国际化演示"
},
"demo": {
"date": {
"title": "日期格式化",
"current": "当前日期"
},
"number": {
"title": "数字格式化",
"price": "产品价格"
},
"plural": {
"title": "复数形式",
"message": "{count, plural, =0 {没有消息} =1 {一条消息} other {# 条消息}}"
},
"nested": {
"title": "嵌套翻译",
"content": "这是一个包含 {placeholder} 的嵌套翻译示例。"
},
"router": {
"title": "路由链接",
"home": "首页",
"about": "关于"
}
}
}

View File

@ -3,13 +3,34 @@ import { ViteSSG } from 'vite-ssg'
import { createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './router/routes'
import i18n from './locales/i18n' // 导入i18n配置
import './style.css'
// 修正:明确 meta.title 的类型为 string
declare module 'vue-router' {
interface RouteMeta {
title?: string // 明确指定为 string 类型
requiresAuth?: boolean
}
}
export const createApp = ViteSSG(
App,
{
history: createWebHistory(),
routes
routes,
base: import.meta.env.BASE_URL || '/'
},
ctx => {
// 安装 i18n 插件
ctx.app.use(i18n)
// 路由守卫:设置页面标题
ctx.router.beforeEach((to, _from, next) => {
// 动态设置页面标题
if (to.meta.title) {
document.title = to.meta.title || '默认标题' // 确保赋值为 string
}
next()
})
}
// _ctx => {
// // 确保路由插件被安装

View File

@ -2,8 +2,17 @@
import type { RouteRecordRaw } from 'vue-router' // 添加type关键字
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import I18nDemo from '../views/I18nDemo.vue'
const routes: RouteRecordRaw[] = [
{
path: '/i18n-demo',
name: 'I18nDemo',
component: I18nDemo,
meta: {
title: '国际化演示'
}
},
{
path: '/',
name: 'Home',

127
src/views/I18nDemo.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<div class="i18n-demo">
<!-- 语言切换按钮 -->
<div class="language-selector">
<button
v-for="lang in availableLanguages"
:key="lang"
:class="{ active: currentLocale === lang }"
@click="switchLocale(lang)"
>
{{ lang === 'en' ? 'English' : '简体中文' }}
</button>
</div>
<!-- 页面内容 -->
<h1>{{ t('page.title') }}</h1>
<!-- 日期格式化示例 -->
<div class="date-demo">
<h3>{{ t('demo.date.title') }}</h3>
<!-- 使用 t 函数格式化日期 -->
<p>{{ t('demo.date.current', { now: new Date() }) }}</p>
<p>{{ t('demo.date.current', { now: nowFormatted }) }}</p>
</div>
<!-- 数字格式化示例 -->
<div class="number-demo">
<h3>{{ t('demo.number.title') }}</h3>
<!-- 使用 t 函数格式化数字 -->
<p>{{ t('demo.number.price', { amount: 12345.67 }) }}</p>
</div>
<!-- 复数示例 -->
<div class="plural-demo">
<h3>{{ t('demo.plural.title') }}</h3>
<!-- {{ t('demo.plural.message', 12) }}-->
<!-- <p v-for="count in [0, 1, 2, 5]" :key="count">-->
<!-- {{ t('demo.plural.message', { count }) }}-->
<!-- </p>-->
</div>
<!-- 嵌套翻译示例 -->
<div class="nested-demo">
<h3>{{ t('demo.nested.title') }}</h3>
<p>{{ t('demo.nested.content') }}</p>
</div>
<!-- 路由链接示例 -->
<div class="router-demo">
<h3>{{ t('demo.router.title') }}</h3>
<router-link to="/">{{ t('demo.router.home') }}</router-link>
<router-link to="/about">{{ t('demo.router.about') }}</router-link>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { ref, computed, onMounted } from 'vue' //
// 使 i18n API
const { t, locale, d } = useI18n()
const nowFormatted = d(new Date(), 'short')
//
const availableLanguages = ref(['en', 'zh-CN'])
//
const switchLocale = (newLocale: string) => {
locale.value = newLocale
console.log(newLocale)
console.log(t)
//
localStorage.setItem('app-locale', newLocale)
}
//
const currentLocale = computed(() => locale.value)
//
onMounted(() => {
const savedLocale = localStorage.getItem('app-locale')
if (savedLocale && availableLanguages.value.includes(savedLocale)) {
locale.value = savedLocale
}
})
</script>
<style scoped>
.i18n-demo {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.language-selector {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #f0f0f0;
cursor: pointer;
transition: background-color 0.3s;
}
button.active {
background-color: #42b983;
color: white;
}
div > h3 {
margin-top: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.router-demo a {
margin-right: 15px;
color: #42b983;
}
</style>

View File

@ -17,7 +17,7 @@
// Vue JSX
"types": [
"vite/client",
"vue"
"vue","vue-i18n"
],
"incremental": true, //
// Vue