添加国际化支持:在多个页面和组件中引入 vue-i18n

This commit is contained in:
jxxghp
2025-04-27 20:27:45 +08:00
parent 733d74ac36
commit f809c8e538
12 changed files with 300 additions and 105 deletions

View File

@@ -1,19 +1,16 @@
<script setup lang="ts">
import NoDataFound from '@/components/NoDataFound.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
</script>
<template>
<NoDataFound
error-code="404"
error-title="页面不存在 "
error-description="您想要访问的页面不存在请检查地址是否正确"
>
<NoDataFound error-code="404" :error-title="t('notFound.title')" :error-description="t('notFound.description')">
<template #button>
<VBtn
to="/"
class="mt-10"
>
返回
<VBtn to="/" class="mt-10">
{{ t('notFound.backButton') }}
</VBtn>
</template>
</NoDataFound>

View File

@@ -2,6 +2,10 @@
import { NavMenu } from '@/@layouts/types'
import { getNavMenus } from '@/router/i18n-menu'
import { useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 从 Store 中获取superuser信息
const superUser = useUserStore().superUser
@@ -18,7 +22,7 @@ function categorizeApps() {
const groupedMenus: Record<string, NavMenu[]> = {}
menus.forEach(menu => {
const header = menu.header || '其他'
const header = menu.header || t('appcenter.others')
if (!groupedMenus[header]) {
groupedMenus[header] = []
}

View File

@@ -1,6 +1,10 @@
<script setup lang="ts">
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
import PersonCardListView from '@/views/discover/PersonCardListView.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 输入参数
const props = defineProps({
@@ -16,7 +20,7 @@ let title = route.query?.title?.toString()
// 类型
const type = route.query?.type?.toString()
if (type === 'person') title = '演员:' + title
if (type === 'person') title = t('browse.actor') + ': ' + title
// 计算API路径
function getApiPath(paths: string[] | string) {

View File

@@ -7,6 +7,10 @@ import BangumiView from '@/views/discover/BangumiView.vue'
import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
import { DiscoverSource } from '@/api/types'
import api from '@/api'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
const activeTab = ref('')
@@ -180,13 +184,13 @@ onActivated(async () => {
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-order-alphabetical-ascending" size="small" class="me-2" />
设置标签顺序
{{ t('discover.setTabOrder') }}
</VCardTitle>
<VDialogCloseBtn @click="orderConfigDialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<p class="settings-hint">拖动对标签页进行排序</p>
<p class="settings-hint">{{ t('discover.dragToReorder') }}</p>
<draggable
v-model="discoverTabs"
handle=".cursor-move"
@@ -210,7 +214,7 @@ onActivated(async () => {
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCard>

View File

@@ -3,6 +3,10 @@ import api from '@/api'
import { DownloaderConf } from '@/api/types'
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
const route = useRoute()
const activeTab = ref(route.query.tab)
@@ -54,7 +58,7 @@ onActivated(async () => {
<NoDataFound
v-else
error-code="404"
error-title="没有下载器"
error-description="请先在设置中正确配置并启用下载器"
:error-title="t('downloading.noDownloader')"
:error-description="t('downloading.configureDownloader')"
/>
</template>

View File

@@ -2,94 +2,98 @@
import api from '@/api'
import { RecommendSource } from '@/api/types'
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 当前选择的分类
const currentCategory = ref('全部')
const currentCategory = ref(t('recommend.all'))
const viewList = reactive<{ apipath: string; linkurl: string; title: string; type: string }[]>([
{
apipath: 'recommend/tmdb_trending',
linkurl: '/browse/recommend/tmdb_trending?title=流行趋势',
title: '流行趋势',
type: '榜单',
linkurl: '/browse/recommend/tmdb_trending?title=' + t('recommend.trendingNow'),
title: t('recommend.trendingNow'),
type: t('recommend.categoryRankings'),
},
{
apipath: 'recommend/douban_showing',
linkurl: '/browse/recommend/douban_showing?title=正在热映',
title: '正在热映',
type: '电影',
linkurl: '/browse/recommend/douban_showing?title=' + t('recommend.nowShowing'),
title: t('recommend.nowShowing'),
type: t('recommend.categoryMovie'),
},
{
apipath: 'recommend/bangumi_calendar',
linkurl: '/browse/recommend/bangumi_calendar?title=Bangumi每日放送',
title: 'Bangumi每日放送',
type: '动漫',
linkurl: '/browse/recommend/bangumi_calendar?title=' + t('recommend.bangumiDaily'),
title: t('recommend.bangumiDaily'),
type: t('recommend.categoryAnime'),
},
{
apipath: 'recommend/tmdb_movies',
linkurl: '/browse/recommend/tmdb_movies?title=TMDB热门电影',
title: 'TMDB热门电影',
type: '电影',
linkurl: '/browse/recommend/tmdb_movies?title=' + t('recommend.tmdbHotMovies'),
title: t('recommend.tmdbHotMovies'),
type: t('recommend.categoryMovie'),
},
{
apipath: 'recommend/tmdb_tvs?with_original_language=zh|en|ja|ko',
linkurl: '/browse/recommend/tmdb_tvs??with_original_language=zh|en|ja|ko&title=TMDB热门电视剧',
title: 'TMDB热门电视剧',
type: '电视剧',
linkurl: '/browse/recommend/tmdb_tvs??with_original_language=zh|en|ja|ko&title=' + t('recommend.tmdbHotTVShows'),
title: t('recommend.tmdbHotTVShows'),
type: t('recommend.categoryTV'),
},
{
apipath: 'recommend/douban_movie_hot',
linkurl: '/browse/recommend/douban_movie_hot?title=豆瓣热门电影',
title: '豆瓣热门电影',
type: '电影',
linkurl: '/browse/recommend/douban_movie_hot?title=' + t('recommend.doubanHotMovies'),
title: t('recommend.doubanHotMovies'),
type: t('recommend.categoryMovie'),
},
{
apipath: 'recommend/douban_tv_hot',
linkurl: '/browse/recommend/douban_tv_hot?title=豆瓣热门电视剧',
title: '豆瓣热门电视剧',
type: '电视剧',
linkurl: '/browse/recommend/douban_tv_hot?title=' + t('recommend.doubanHotTVShows'),
title: t('recommend.doubanHotTVShows'),
type: t('recommend.categoryTV'),
},
{
apipath: 'recommend/douban_tv_animation',
linkurl: '/browse/recommend/douban_tv_animation?title=豆瓣热门动漫',
title: '豆瓣热门动漫',
type: '动漫',
linkurl: '/browse/recommend/douban_tv_animation?title=' + t('recommend.doubanHotAnime'),
title: t('recommend.doubanHotAnime'),
type: t('recommend.categoryAnime'),
},
{
apipath: 'recommend/douban_movies',
linkurl: '/browse/recommend/douban_movies?title=豆瓣最新电影',
title: '豆瓣最新电影',
type: '电影',
linkurl: '/browse/recommend/douban_movies?title=' + t('recommend.doubanNewMovies'),
title: t('recommend.doubanNewMovies'),
type: t('recommend.categoryMovie'),
},
{
apipath: 'recommend/douban_tvs',
linkurl: '/browse/recommend/douban_tvs?title=豆瓣最新电视剧',
title: '豆瓣最新电视剧',
type: '电视剧',
linkurl: '/browse/recommend/douban_tvs?title=' + t('recommend.doubanNewTVShows'),
title: t('recommend.doubanNewTVShows'),
type: t('recommend.categoryTV'),
},
{
apipath: 'recommend/douban_movie_top250',
linkurl: '/browse/recommend/douban_movie_top250?title=电影TOP250',
title: '豆瓣电影TOP250',
type: '榜单',
linkurl: '/browse/recommend/douban_movie_top250?title=' + t('recommend.doubanTop250'),
title: t('recommend.doubanTop250'),
type: t('recommend.categoryRankings'),
},
{
apipath: 'recommend/douban_tv_weekly_chinese',
linkurl: '/browse/recommend/douban_tv_weekly_chinese?title=豆瓣国产剧集榜',
title: '豆瓣国产剧集榜',
type: '榜单',
linkurl: '/browse/recommend/douban_tv_weekly_chinese?title=' + t('recommend.doubanChineseTVRankings'),
title: t('recommend.doubanChineseTVRankings'),
type: t('recommend.categoryRankings'),
},
{
apipath: 'recommend/douban_tv_weekly_global',
linkurl: '/browse/recommend/douban_tv_weekly_global?title=豆瓣全球剧集榜',
title: '豆瓣全球剧集榜',
type: '榜单',
linkurl: '/browse/recommend/douban_tv_weekly_global?title=' + t('recommend.doubanGlobalTVRankings'),
title: t('recommend.doubanGlobalTVRankings'),
type: t('recommend.categoryRankings'),
},
])
// 计算当前分类下显示的视图
const filteredViews = computed(() => {
if (currentCategory.value === '全部') {
if (currentCategory.value === t('recommend.all')) {
return viewList.filter(item => enableConfig.value[item.title])
}
return viewList.filter(item => enableConfig.value[item.title] && item.type === currentCategory.value)
@@ -158,29 +162,29 @@ async function saveConfig() {
// 标签图标映射
const categoryItems: Record<string, string>[] = [
{
title: '全部',
title: t('recommend.all'),
icon: 'mdi-filmstrip-box-multiple',
tab: '全部',
tab: t('recommend.all'),
},
{
title: '电影',
title: t('recommend.categoryMovie'),
icon: 'mdi-movie',
tab: '电影',
tab: t('recommend.categoryMovie'),
},
{
title: '电视剧',
title: t('recommend.categoryTV'),
icon: 'mdi-television-classic',
tab: '电视剧',
tab: t('recommend.categoryTV'),
},
{
title: '动漫',
title: t('recommend.categoryAnime'),
icon: 'mdi-animation',
tab: '动漫',
tab: t('recommend.categoryAnime'),
},
{
title: '榜单',
title: t('recommend.categoryRankings'),
icon: 'mdi-trophy',
tab: '榜单',
tab: t('recommend.categoryRankings'),
},
]
@@ -221,8 +225,10 @@ onActivated(async () => {
<div v-if="filteredViews.length === 0" class="empty-category">
<VIcon icon="mdi-alert-circle-outline" size="large" class="empty-icon" />
<p class="empty-text">当前分类下没有可显示的内容</p>
<VBtn color="primary" variant="tonal" size="small" @click="dialog = true"> 设置显示内容 </VBtn>
<p class="empty-text">{{ t('recommend.noCategoryContent') }}</p>
<VBtn color="primary" variant="tonal" size="small" @click="dialog = true">
{{ t('recommend.configureContent') }}
</VBtn>
</div>
</div>
@@ -232,13 +238,13 @@ onActivated(async () => {
<VCardItem class="settings-card-header">
<VCardTitle>
<VIcon icon="mdi-tune" size="small" class="me-2" />
自定义内容
{{ t('recommend.customizeContent') }}
</VCardTitle>
<VDialogCloseBtn @click="dialog = false" />
</VCardItem>
<VDivider />
<VCardText>
<p class="settings-hint">选择您想在页面显示的内容</p>
<p class="settings-hint">{{ t('recommend.selectContentToDisplay') }}</p>
<div class="settings-grid">
<div
v-for="(item, index) in viewList"
@@ -266,17 +272,17 @@ onActivated(async () => {
<VDivider />
<VCardActions class="pt-5">
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = true))">
全选
{{ t('recommend.selectAll') }}
</VBtn>
<VBtn variant="text" @click="Object.keys(enableConfig).forEach(key => (enableConfig[key] = false))">
全不选
{{ t('recommend.selectNone') }}
</VBtn>
<VSpacer />
<VBtn @click="saveConfig" variant="elevated" color="primary" class="px-5">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
保存
{{ t('common.save') }}
</VBtn>
</VCardActions>
</VCard>

View File

@@ -4,6 +4,10 @@ import api from '@/api'
import type { Context } from '@/api/types'
import TorrentCardListView from '@/views/torrent/TorrentCardListView.vue'
import TorrentRowListView from '@/views/torrent/TorrentRowListView.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 路由参数
const route = useRoute()
@@ -51,14 +55,14 @@ const progressValue = ref(0)
const progressEventSource = ref<EventSource>()
// 错误标题
const errorTitle = ref('没有数据')
const errorTitle = ref(t('resource.noData'))
// 错误描述
const errorDescription = ref('未搜索到任何资源')
const errorDescription = ref(t('resource.noResourceFound'))
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '正在搜索,请稍候...'
progressText.value = t('resource.searching')
progressValue.value = 10 // 初始进度设为10%,确保进度条显示
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/search`)
progressEventSource.value.onmessage = event => {
@@ -199,14 +203,20 @@ onUnmounted(() => {
<!-- 精简标题栏 -->
<VCard v-if="isRefreshed" class="search-header d-flex align-center mb-3">
<div class="search-info-container d-flex align-center flex-wrap">
<div class="search-title text-primary">资源搜索结果</div>
<div class="search-title text-primary">{{ t('resource.searchResults') }}</div>
<div class="search-tags d-flex flex-wrap">
<VChip v-if="keyword" class="search-tag" color="primary" size="small" variant="flat">
关键词: {{ keyword }}
{{ t('resource.keyword') }}: {{ keyword }}
</VChip>
<VChip v-if="title" class="search-tag" color="primary" size="small" variant="flat">
{{ t('resource.title') }}: {{ title }}
</VChip>
<VChip v-if="year" class="search-tag" color="primary" size="small" variant="flat">
{{ t('resource.year') }}: {{ year }}
</VChip>
<VChip v-if="season" class="search-tag" color="primary" size="small" variant="flat">
{{ t('resource.season') }}: {{ season }}
</VChip>
<VChip v-if="title" class="search-tag" color="primary" size="small" variant="flat"> 标题: {{ title }} </VChip>
<VChip v-if="year" class="search-tag" color="primary" size="small" variant="flat"> 年份: {{ year }} </VChip>
<VChip v-if="season" class="search-tag" color="primary" size="small" variant="flat"> : {{ season }} </VChip>
</div>
</div>
<VSpacer />
@@ -232,7 +242,7 @@ onUnmounted(() => {
<div class="pulse-circle"></div>
<div class="pulse-circle"></div>
</div>
<div class="view-changing-text">切换视图</div>
<div class="view-changing-text">{{ t('resource.switchingView') }}</div>
</div>
</div>
</VFadeTransition>
@@ -257,7 +267,7 @@ onUnmounted(() => {
<!-- 无数据显示 -->
<div v-else-if="isRefreshed && !isViewChanging" class="d-flex flex-column align-center justify-center py-8">
<NoDataFound :errorTitle="errorTitle" :errorDescription="errorDescription" />
<VBtn class="mt-4" color="primary" prepend-icon="mdi-magnify" to="/"> 返回首页 </VBtn>
<VBtn class="mt-4" color="primary" prepend-icon="mdi-magnify" to="/">{{ t('resource.backToHome') }}</VBtn>
</div>
<!-- 初始加载状态 -->
@@ -269,7 +279,7 @@ onUnmounted(() => {
<div class="wave-dot"></div>
<div class="wave-dot"></div>
</div>
<div class="initial-loading-text">搜索中</div>
<div class="initial-loading-text">{{ t('resource.searching') }}</div>
</div>
</div>
<!-- 滚动到顶部按钮 -->

View File

@@ -3,9 +3,13 @@ import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
import SubscribeShareView from '@/views/subscribe/SubscribeShareView.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import { useI18n } from 'vue-i18n'
import { getSubscribeMovieTabs, getSubscribeTvTabs } from '@/router/i18n-menu'
// 国际化
const { t } = useI18n()
const route = useRoute()
const subType = route.meta.subType?.toString()
@@ -69,12 +73,12 @@ const searchShares = () => {
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
筛选订阅
{{ t('subscribe.filterSubscriptions') }}
</VCardTitle>
<VDialogCloseBtn @click="filterSubscribeDialog = false" />
</VCardItem>
<VCardText>
<VTextField v-model="subscribeFilter" label="名称" clearable density="comfortable" />
<VTextField v-model="subscribeFilter" :label="t('subscribe.name')" clearable density="comfortable" />
</VCardText>
</VCard>
</VMenu>
@@ -99,14 +103,14 @@ const searchShares = () => {
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-movie-search-outline" class="mr-2" />
搜索订阅分享
{{ t('subscribe.searchShares') }}
</VCardTitle>
<VDialogCloseBtn @click="searchShareDialog = false" />
</VCardItem>
<VCardText>
<VTextField v-model="shareKeyword" label="关键词" clearable density="comfortable">
<VTextField v-model="shareKeyword" :label="t('subscribe.keyword')" clearable density="comfortable">
<template #append>
<VBtn prepend-icon="mdi-magnify" color="primary" @click="searchShares">搜索</VBtn>
<VBtn prepend-icon="mdi-magnify" color="primary" @click="searchShares">{{ t('common.search') }}</VBtn>
</template>
</VTextField>
</VCardText>