feat: 20250611 配置 @intlify/unplugin-vue-i18n,国际化I18n多语言
This commit is contained in:
parent
420fd4f3ff
commit
481ec5ecf7
@ -25,11 +25,13 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"postcss-px-to-viewport": "^1.1.1",
|
"postcss-px-to-viewport": "^1.1.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-i18n": "^9.8.0",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.8.1",
|
"@commitlint/cli": "^19.8.1",
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
|
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"@typescript-eslint/eslint-plugin": "6.13.2",
|
"@typescript-eslint/eslint-plugin": "6.13.2",
|
||||||
"@typescript-eslint/parser": "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 { createWebHistory } from 'vue-router'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import routes from './router/routes'
|
import routes from './router/routes'
|
||||||
|
import i18n from './locales/i18n' // 导入i18n配置
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
// 修正:明确 meta.title 的类型为 string
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
title?: string // 明确指定为 string 类型
|
||||||
|
requiresAuth?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
export const createApp = ViteSSG(
|
export const createApp = ViteSSG(
|
||||||
App,
|
App,
|
||||||
{
|
{
|
||||||
history: createWebHistory(),
|
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 => {
|
// _ctx => {
|
||||||
// // 确保路由插件被安装
|
// // 确保路由插件被安装
|
||||||
|
|||||||
@ -2,8 +2,17 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router' // 添加type关键字
|
import type { RouteRecordRaw } from 'vue-router' // 添加type关键字
|
||||||
import Home from '@/views/Home.vue'
|
import Home from '@/views/Home.vue'
|
||||||
import About from '@/views/About.vue'
|
import About from '@/views/About.vue'
|
||||||
|
import I18nDemo from '../views/I18nDemo.vue'
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/i18n-demo',
|
||||||
|
name: 'I18nDemo',
|
||||||
|
component: I18nDemo,
|
||||||
|
meta: {
|
||||||
|
title: '国际化演示'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
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 语法
|
// 支持 Vue 的 JSX 语法
|
||||||
"types": [
|
"types": [
|
||||||
"vite/client",
|
"vite/client",
|
||||||
"vue"
|
"vue","vue-i18n"
|
||||||
],
|
],
|
||||||
"incremental": true, // 添加这一行
|
"incremental": true, // 添加这一行
|
||||||
// 新增 Vue 类型声明
|
// 新增 Vue 类型声明
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user