Commit c9b259a1 authored by Administrator's avatar Administrator
Browse files

added i18n support for Chinese for student page.

parent 0caedf2f
......@@ -12,6 +12,7 @@
"axios": "^1.11.0",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-i18n": "^9.14.5",
"vue-router": "^4.5.1",
"vuetify": "^3.9.5"
},
......@@ -1191,6 +1192,50 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@intlify/core-base": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
"integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "9.14.5",
"@intlify/shared": "9.14.5"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
"integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "9.14.5",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz",
"integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
......@@ -4342,6 +4387,32 @@
"eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/vue-i18n": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz",
"integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "9.14.5",
"@intlify/shared": "9.14.5",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-i18n/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
......
......@@ -18,6 +18,7 @@
"axios": "^1.11.0",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-i18n": "^9.14.5",
"vue-router": "^4.5.1",
"vuetify": "^3.9.5"
},
......
......@@ -5,11 +5,11 @@
app
width="250"
>
<v-list-item title="Welcome to Students App" subtitle="powered by vuetify"></v-list-item>
<v-list-item :title="$t('navigation.welcome.title')" :subtitle="$t('navigation.welcome.subtitle')"></v-list-item>
<v-divider></v-divider>
<v-list v-model:opened="opened">
<template v-for="item in filteredMenu" :key="item.path || item.name">
<template v-for="item in filteredMenu" :key="item.path || item.nameKey">
<!-- 分组(有 children):点击父项仅用于展开,不路由 -->
<v-list-group
v-if="item.children && item.children.length"
......@@ -18,20 +18,20 @@
>
<template #activator="{ props }">
<v-list-item v-bind="props">
<v-list-item-title class="text-body-1">{{ item.name }}</v-list-item-title>
<v-list-item-title class="text-body-1">{{ $t(item.nameKey) }}</v-list-item-title>
</v-list-item>
</template>
<v-list-item
v-for="child in item.children"
:key="child.path || child.name"
:key="child.path || child.nameKey"
:prepend-icon="child.mdiIcon"
:to="child.hasRouter ? child.path : undefined"
:ripple="child.hasRouter"
:disabled="!child.hasRouter"
nav
>
<v-list-item-title class="text-body-1">{{ child.name }}</v-list-item-title>
<v-list-item-title class="text-body-1">{{ $t(child.nameKey) }}</v-list-item-title>
</v-list-item>
</v-list-group>
......@@ -44,7 +44,7 @@
:disabled="false"
@click="!item.hasRouter ? noop() : undefined"
nav>
<v-list-item-title class="text-body-1">{{ item.name }}</v-list-item-title>
<v-list-item-title class="text-body-1">{{ $t(item.nameKey) }}</v-list-item-title>
</v-list-item>
</template>
</v-list>
......@@ -55,8 +55,12 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { GLOBAL_MENU_ITEMS } from './constants/menuItems'
// 使用i18n的全局实例
useI18n()
defineProps({
modelValue: Boolean
})
......
......@@ -10,16 +10,18 @@
<v-btn icon>
<v-icon>mdi-bell</v-icon>
</v-btn>
<LanguageSwitcher class="mr-2" />
<v-btn icon @click="$emit('logout')">
<v-icon>mdi-logout</v-icon>
</v-btn>
</template>
<v-app-bar-title>Student Management Application</v-app-bar-title>
<v-app-bar-title>{{ $t('components.appHeader.title') }}</v-app-bar-title>
</v-app-bar>
</template>
<script setup>
import LanguageSwitcher from './LanguageSwitcher.vue'
</script>
<style scoped>
......
......@@ -34,12 +34,12 @@
<!-- 登出确认对话框 -->
<v-dialog v-model="showLogoutDialog" max-width="400">
<v-card>
<v-card-title>确认登出</v-card-title>
<v-card-text>你确定要退出登录吗?</v-card-text>
<v-card-title>{{ $t('components.appHeader.logoutDialog.title') }}</v-card-title>
<v-card-text>{{ $t('components.appHeader.logoutDialog.message') }}</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" text @click="showLogoutDialog = false">取消</v-btn>
<v-btn color="primary" text @click="confirmLogout">确认</v-btn>
<v-btn color="primary" text @click="showLogoutDialog = false">{{ $t('components.appHeader.logoutDialog.cancel') }}</v-btn>
<v-btn color="primary" text @click="confirmLogout">{{ $t('components.appHeader.logoutDialog.confirm') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
......@@ -48,6 +48,7 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import AppHeader from './AppHeader.vue'
import AppDrawer from './AppDrawer.vue'
import AppFooter from './AppFooter.vue'
......@@ -56,6 +57,7 @@ import { useAuthStore } from '../stores/auth'
import { useRouter, useRoute } from 'vue-router'
import { GLOBAL_MENU_ITEMS } from './constants/menuItems'
const { t } = useI18n()
const auth = useAuthStore()
const router = useRouter()
const route = useRoute()
......@@ -86,17 +88,30 @@ onMounted(() => {
})
const breadcrumbs = computed(() => {
const matched = route.matched.filter(r => r.meta && r.meta.title)
const items = matched.map((r, index) => ({
title: r.meta.title,
to: r.path,
disabled: index === matched.length - 1,
}))
// 如果没有任何匹配标题,至少返回 Home
if (items.length === 0) {
return [{ title: 'Home', to: '/', disabled: route.path === '/' }]
const path = route.path
// 根据路径生成面包屑
if (path === '/') {
return [{ title: t('navigation.breadcrumb.home'), to: '/', disabled: true }]
}
if (path === '/profile') {
return [
{ title: t('navigation.breadcrumb.home'), to: '/', disabled: false },
{ title: t('navigation.breadcrumb.profile'), to: '/profile', disabled: true }
]
}
return items
if (path === '/master-data/students') {
return [
{ title: t('navigation.breadcrumb.home'), to: '/', disabled: false },
{ title: t('navigation.breadcrumb.masterData'), to: null, disabled: true },
{ title: t('navigation.breadcrumb.students'), to: '/master-data/students', disabled: true }
]
}
// 默认返回首页
return [{ title: t('navigation.breadcrumb.home'), to: '/', disabled: true }]
})
const currentBreadcrumbIcon = computed(() => {
......
<template>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
variant="text"
prepend-icon="mdi-translate"
class="text-none"
>
{{ getCurrentLanguageName() }}
<v-icon class="ml-1">mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-list min-width="150">
<v-list-item
v-for="locale in supportedLocales"
:key="locale.code"
:active="locale.code === currentLocale"
@click="switchLanguage(locale.code)"
>
<v-list-item-title>{{ locale.name }}</v-list-item-title>
<template v-slot:append>
<v-icon v-if="locale.code === currentLocale" color="primary">
mdi-check
</v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
</template>
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { supportedLocales, setLocale, getCurrentLocale } from '@/locales'
// 使用i18n的全局实例
useI18n()
const currentLocale = computed(() => getCurrentLocale())
function getCurrentLanguageName() {
const current = supportedLocales.find(l => l.code === currentLocale.value)
return current?.name || 'Language'
}
function switchLanguage(localeCode) {
setLocale(localeCode)
}
</script>
export const GLOBAL_MENU_ITEMS = [
{
name: 'Home',
nameKey: 'navigation.menu.home',
path: '/',
mdiIcon: 'mdi-home',
hasRouter: true
},
{
name: 'Profile',
nameKey: 'navigation.menu.profile',
path: '/profile',
mdiIcon: 'mdi-account',
hasRouter: true
},
{
name: 'Master Data',
nameKey: 'navigation.menu.masterData',
path: '/master-data',
mdiIcon: 'mdi-database-outline',
hasRouter: false,
children: [
{
name: 'Students',
nameKey: 'navigation.menu.students',
path: '/master-data/students',
mdiIcon: 'mdi-account-school',
hasRouter: true
},
{
name: 'Subjects',
nameKey: 'navigation.menu.subjects',
path: '/subjects',
mdiIcon: 'mdi-book-open-blank-variant-outline',
hasRouter: true
......
export default {
save: 'Save',
cancel: 'Cancel',
delete: 'Delete',
edit: 'Edit',
create: 'Create',
add: 'Add',
refresh: 'Refresh',
upload: 'Upload',
change: 'Change',
remove: 'Remove',
confirm: 'Confirm',
close: 'Close',
ok: 'OK',
yes: 'Yes',
no: 'No'
}
export default {
success: {
save: 'Saved successfully',
create: 'Created successfully',
update: 'Updated successfully',
delete: 'Deleted successfully',
upload: 'Uploaded successfully'
},
error: {
general: 'An error occurred, please try again',
network: 'Network error, please try again',
unauthorized: 'Access denied',
notFound: 'Resource not found',
validation: 'Please check the input data',
upload: 'Upload failed',
save: 'Save failed, please try again',
create: 'Creation failed, please try again',
delete: 'Deletion failed, please try again'
},
loading: {
general: 'Loading...',
saving: 'Saving...',
creating: 'Creating...',
deleting: 'Deleting...',
uploading: 'Uploading...'
},
confirmation: {
delete: 'Are you sure you want to delete this item?',
unsavedChanges: 'You have unsaved changes. Are you sure you want to leave?',
irreversible: 'This operation cannot be undone. Please proceed with caution.'
}
}
export default {
enabled: 'Enabled',
disabled: 'Disabled',
active: 'Active',
inactive: 'Inactive',
online: 'Online',
offline: 'Offline',
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected',
completed: 'Completed',
inProgress: 'In Progress',
cancelled: 'Cancelled'
}
export default {
required: 'This field is required',
email: 'Please enter a valid email address',
minLength: 'Minimum length is {min} characters',
maxLength: 'Maximum length is {max} characters',
numeric: 'Please enter a valid number',
positive: 'Please enter a positive number',
fileSize: 'File size cannot exceed {size}',
fileType: 'Please select a valid file type',
imageFile: 'Please select an image file',
passwordMatch: 'Passwords do not match',
uniqueValue: 'This value already exists'
}
export default {
title: 'Student Management System',
search: {
placeholder: 'Search...',
button: 'Search'
},
notifications: {
button: 'Notifications',
empty: 'No notifications',
title: 'Notifications'
},
user: {
menu: 'User menu',
profile: 'Profile',
settings: 'Settings',
logout: 'Logout'
},
// 登出对话框
logoutDialog: {
title: 'Confirm Logout',
message: 'Are you sure you want to logout?',
confirm: 'Confirm',
cancel: 'Cancel'
}
}
// 导入所有翻译模块
import buttons from './common/buttons'
import messages from './common/messages'
import status from './common/status'
import validation from './common/validation'
import student from './modules/student'
import auth from './modules/auth'
import navigation from './modules/navigation'
import appHeader from './components/app-header'
// 组合所有翻译
export default {
common: {
buttons,
messages,
status,
validation
},
student,
auth,
navigation,
components: {
appHeader
}
}
export default {
login: {
title: 'Welcome!',
description: 'Welcome to the Student Management System! This system is a one-stop service platform for tracking student courses, assignments, and exams. Please log in to use all features.',
form: {
username: 'Username',
password: 'Password',
rememberMe: 'Remember me',
forgotPassword: 'Forgot password?'
},
buttons: {
signIn: 'Login',
signUp: 'Sign Up'
},
messages: {
success: 'Login successful',
error: 'Login failed, please check your credentials',
unauthorized: 'Invalid username or password',
fillRequired: 'Please fill in username and password',
loginFailed: 'Login failed',
loginError: 'An error occurred during login'
}
},
profile: {
title: 'Profile',
subtitle: 'Manage your account information',
form: {
displayName: 'Display Name',
email: 'Email',
phone: 'Phone',
bio: 'Bio'
},
messages: {
updateSuccess: 'Profile updated successfully',
updateError: 'Failed to update profile'
}
},
logout: {
title: 'Logout',
message: 'Are you sure you want to logout?',
success: 'Logout successful'
}
}
export default {
// 抽屉欢迎信息
welcome: {
title: 'Welcome to Students App',
subtitle: 'powered by vuetify'
},
menu: {
home: 'Home',
profile: 'Profile',
masterData: 'Master Data',
students: 'Students',
subjects: 'Subjects',
dashboard: 'Dashboard',
settings: 'Settings',
about: 'About'
},
breadcrumb: {
home: 'Home',
masterData: 'Master Data',
students: 'Students',
profile: 'Profile'
},
drawer: {
title: 'Navigation',
collapse: 'Collapse menu',
expand: 'Expand menu'
}
}
export default {
title: 'Students',
subtitle: 'Student Management',
// 表格标题
table: {
headers: {
avatar: 'Avatar',
name: 'Name',
age: 'Age',
grade: 'Grade',
enabled: 'Status',
actions: 'Actions'
},
// 数据表分页
pagination: {
itemsPerPage: 'Items per page:',
itemsPerPageAll: 'All',
itemsPerPageText: '{start}-{end} of {total}',
pageText: 'Page {page} of {pages}',
noDataText: 'No data available',
loadingText: 'Loading items...'
}
},
// 表单字段
form: {
name: 'Name',
age: 'Age',
grade: 'Grade',
enabled: 'Status',
enabledStatus: 'Enabled Status',
avatar: 'Avatar',
namePlaceholder: 'Please enter student name',
agePlaceholder: 'Please enter age',
gradePlaceholder: 'Please enter grade'
},
// 对话框标题
dialog: {
add: 'Add Student',
edit: 'Edit Student Information',
delete: 'Confirm Delete'
},
// 按钮文本
buttons: {
addStudent: 'Add Student',
uploadAvatar: 'Upload Avatar',
changeAvatar: 'Change Avatar'
},
// 消息
messages: {
createSuccess: 'Student created successfully',
updateSuccess: 'Student information updated successfully',
deleteSuccess: 'Student "{name}" deleted successfully',
createError: 'Creation failed, please try again',
updateError: 'Save failed, please try again',
deleteError: 'Deletion failed, please try again',
loadError: 'Failed to load students'
},
// 删除确认
deleteConfirmation: {
title: 'Confirm Delete',
message: 'Are you sure you want to delete this student?',
warning: 'This operation cannot be undone. Please proceed with caution.',
studentInfo: '{name} | {age} years old | {grade}'
},
// 头像上传
avatar: {
upload: {
title: 'Avatar',
button: 'Upload Avatar',
change: 'Change Avatar',
remove: 'Remove Avatar',
formats: 'Supports PNG, JPG, GIF formats',
sizeLimit: 'File size up to 2MB',
selectImage: 'Please select an image file',
sizeExceeded: 'File size cannot exceed 2MB',
readError: 'File reading failed, please try again'
},
fallback: {
alt: 'avatar',
placeholder: 'No Avatar'
}
},
// 验证消息
validation: {
nameRequired: 'Name is required',
enabledRequired: 'Status is required'
}
}
import { createI18n } from 'vue-i18n'
import en from './en'
import zhCN from './zh-CN'
// 默认语言
const defaultLocale = 'en'
// 支持的语言列表
export const supportedLocales = [
{ code: 'en', name: 'English' },
{ code: 'zh-CN', name: '中文' }
]
// 从localStorage获取用户偏好语言
function getStoredLocale() {
const stored = localStorage.getItem('preferred-locale')
if (stored && supportedLocales.some(locale => locale.code === stored)) {
return stored
}
return null
}
// 检测浏览器语言
function getBrowserLocale() {
const navigator = window.navigator
const browserLocale = navigator.language || navigator.userLanguage || defaultLocale
// 检查是否直接支持
if (supportedLocales.some(locale => locale.code === browserLocale)) {
return browserLocale
}
// 检查是否支持语言的主要部分(如 zh-CN -> zh)
const primaryLanguage = browserLocale.split('-')[0]
const matchedLocale = supportedLocales.find(locale =>
locale.code.split('-')[0] === primaryLanguage
)
return matchedLocale?.code || defaultLocale
}
// 确定使用的语言
const initialLocale = getStoredLocale() || getBrowserLocale()
// 创建i18n实例
const i18n = createI18n({
legacy: false, // 使用Composition API模式
locale: initialLocale,
fallbackLocale: defaultLocale,
messages: {
en,
'zh-CN': zhCN
},
// 全局注入属性
globalInjection: true,
// 开发环境下显示缺失翻译警告
silentTranslationWarn: process.env.NODE_ENV === 'production',
silentFallbackWarn: process.env.NODE_ENV === 'production'
})
// 语言切换函数
export function setLocale(locale) {
if (supportedLocales.some(l => l.code === locale)) {
i18n.global.locale.value = locale
localStorage.setItem('preferred-locale', locale)
// 设置HTML lang属性
document.documentElement.lang = locale
}
}
// 获取当前语言
export function getCurrentLocale() {
return i18n.global.locale.value
}
// 初始化时设置HTML lang属性
document.documentElement.lang = initialLocale
export default i18n
export default {
save: '保存',
cancel: '取消',
delete: '删除',
edit: '编辑',
create: '创建',
add: '添加',
refresh: '刷新',
upload: '上传',
change: '更换',
remove: '移除',
confirm: '确认',
close: '关闭',
ok: '确定',
yes: '',
no: ''
}
export default {
success: {
save: '保存成功',
create: '创建成功',
update: '更新成功',
delete: '删除成功',
upload: '上传成功'
},
error: {
general: '发生错误,请重试',
network: '网络错误,请重试',
unauthorized: '访问被拒绝',
notFound: '资源不存在',
validation: '请检查输入数据',
upload: '上传失败',
save: '保存失败,请重试',
create: '创建失败,请重试',
delete: '删除失败,请重试'
},
loading: {
general: '加载中...',
saving: '保存中...',
creating: '创建中...',
deleting: '删除中...',
uploading: '上传中...'
},
confirmation: {
delete: '确定要删除此项吗?',
unsavedChanges: '您有未保存的更改,确定要离开吗?',
irreversible: '此操作无法撤销,请谨慎操作。'
}
}
export default {
enabled: '启用',
disabled: '禁用',
active: '活跃',
inactive: '非活跃',
online: '在线',
offline: '离线',
pending: '待处理',
approved: '已批准',
rejected: '已拒绝',
completed: '已完成',
inProgress: '进行中',
cancelled: '已取消'
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment