feat: 20250611 配置 @intlify/unplugin-vue-i18n,国际化I18n多语言
This commit is contained in:
parent
420fd4f3ff
commit
481ec5ecf7
@ -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",
|
||||
|
||||
20
src/locales/en/common.json
Normal file
20
src/locales/en/common.json
Normal 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
18
src/locales/en/home.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
28
src/locales/en/i18nDemo.json
Normal file
28
src/locales/en/i18nDemo.json
Normal 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
61
src/locales/i18n.ts
Normal 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
|
||||
20
src/locales/zh-CN/common.json
Normal file
20
src/locales/zh-CN/common.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"button": {
|
||||
"submit": "提交",
|
||||
"cancel": "取消",
|
||||
"back": "返回",
|
||||
"next": "下一步"
|
||||
},
|
||||
"message": {
|
||||
"welcome": "欢迎使用我们的应用",
|
||||
"loading": "加载中...",
|
||||
"success": "操作成功",
|
||||
"error": "发生错误"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "首页",
|
||||
"about": "关于我们",
|
||||
"contact": "联系我们",
|
||||
"profile": "个人资料"
|
||||
}
|
||||
}
|
||||
18
src/locales/zh-CN/home.json
Normal file
18
src/locales/zh-CN/home.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"title": "首页",
|
||||
"subtitle": "欢迎访问我们的网站",
|
||||
"hero": {
|
||||
"heading": "发现我们的产品",
|
||||
"description": "探索我们的高品质解决方案系列",
|
||||
"cta": "了解更多"
|
||||
},
|
||||
"features": {
|
||||
"title": "核心功能",
|
||||
"items": [
|
||||
"快速性能",
|
||||
"简单集成",
|
||||
"安全交易",
|
||||
"全天候支持"
|
||||
]
|
||||
}
|
||||
}
|
||||
29
src/locales/zh-CN/i18nDemo.json
Normal file
29
src/locales/zh-CN/i18nDemo.json
Normal 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": "关于"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/main.ts
25
src/main.ts
@ -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 => {
|
||||
// // 确保路由插件被安装
|
||||
|
||||
@ -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
127
src/views/I18nDemo.vue
Normal 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>
|
||||
@ -17,7 +17,7 @@
|
||||
// 支持 Vue 的 JSX 语法
|
||||
"types": [
|
||||
"vite/client",
|
||||
"vue"
|
||||
"vue","vue-i18n"
|
||||
],
|
||||
"incremental": true, // 添加这一行
|
||||
// 新增 Vue 类型声明
|
||||
|
||||
Loading…
Reference in New Issue
Block a user