更新国际化支持:为下载器和过滤规则组件添加多语言文本,提升用户体验

This commit is contained in:
jxxghp
2025-04-28 19:54:01 +08:00
parent 99212c1186
commit 40711fa640
6 changed files with 189 additions and 62 deletions

View File

@@ -7,6 +7,10 @@ import type { DownloaderInfo } from '@/api/types'
import qbittorrent_image from '@images/logos/qbittorrent.png'
import transmission_image from '@images/logos/transmission.png'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
// 获取i18n实例
const { t } = useI18n()
// 定义输入
const props = defineProps({
@@ -91,12 +95,12 @@ function openDownloaderInfoDialog() {
function saveDownloaderInfo() {
// 为空不保存,跳出警告框
if (!downloaderInfo.value.name) {
$toast.error('名称不能为空,请输入后再确定')
$toast.error(t('downloader.nameRequired'))
return
}
// 重名判断
if (props.downloaders.some(item => item.name === downloaderInfo.value.name && item !== props.downloader)) {
$toast.error(`${downloaderInfo.value.name}】已存在,请替换为其他名称`)
$toast.error(t('downloader.nameDuplicate'))
return
}
// 默认下载器去重
@@ -104,7 +108,7 @@ function saveDownloaderInfo() {
props.downloaders.forEach(item => {
if (item.default && item !== props.downloader) {
item.default = false
$toast.info(`存在默认下载器【${item.name}】,已替换成【${downloaderInfo.value.name}`)
$toast.info(t('downloader.defaultChanged'))
}
})
}
@@ -180,26 +184,30 @@ onUnmounted(() => {
</VCard>
</VHover>
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
<VCard :title="`${props.downloader.name} - 配置`" class="rounded-t">
<VCard :title="`${props.downloader.name} - ${t('downloader.title')}`" class="rounded-t">
<VDialogCloseBtn v-model="downloaderInfoDialog" />
<VDivider />
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSwitch v-model="downloaderInfo.enabled" label="启用下载器" />
<VSwitch v-model="downloaderInfo.enabled" :label="t('downloader.enabled')" />
</VCol>
<VCol cols="12" md="6">
<VSwitch v-model="downloaderInfo.default" label="默认下载器" :disabled="!downloaderInfo.enabled" />
<VSwitch
v-model="downloaderInfo.default"
:label="t('downloader.default')"
:disabled="!downloaderInfo.enabled"
/>
</VCol>
</VRow>
<VRow v-if="downloaderInfo.type == 'qbittorrent'">
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.name"
label="名称"
placeholder="必填;不可与其他名称重名"
hint="下载器的别名"
:label="t('downloader.name')"
:placeholder="t('downloader.nameRequired')"
:hint="t('downloader.name')"
persistent-hint
active
/>
@@ -207,9 +215,9 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.host"
label="地址"
:label="t('downloader.host')"
placeholder="http(s)://ip:port"
hint="服务端地址格式http(s)://ip:port"
:hint="t('downloader.host')"
persistent-hint
active
/>
@@ -217,8 +225,8 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.username"
label="用户名"
hint="登录使用的用户名"
:label="t('downloader.username')"
:hint="t('downloader.username')"
persistent-hint
active
/>
@@ -227,8 +235,8 @@ onUnmounted(() => {
<VTextField
v-model="downloaderInfo.config.password"
type="password"
label="密码"
hint="登录使用的密码"
:label="t('downloader.password')"
:hint="t('downloader.password')"
persistent-hint
active
/>
@@ -236,8 +244,8 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VSwitch
v-model="downloaderInfo.config.category"
label="自动分类管理"
hint="由下载器自动管理分类和下载目录"
:label="t('downloader.category')"
:hint="t('downloader.category')"
persistent-hint
active
/>
@@ -245,8 +253,8 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VSwitch
v-model="downloaderInfo.config.sequentail"
label="顺序下载"
hint="按顺序依次下载文件"
:label="t('downloader.sequentail')"
:hint="t('downloader.sequentail')"
persistent-hint
active
/>
@@ -254,8 +262,8 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VSwitch
v-model="downloaderInfo.config.force_resume"
label="强制继续"
hint="强制继续、强制上传模式"
:label="t('downloader.force_resume')"
:hint="t('downloader.force_resume')"
persistent-hint
active
/>
@@ -263,8 +271,8 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VSwitch
v-model="downloaderInfo.config.first_last_piece"
label="优先首尾文件"
hint="优先下载首尾文件块"
:label="t('downloader.first_last_piece')"
:hint="t('downloader.first_last_piece')"
persistent-hint
active
/>
@@ -274,9 +282,9 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.name"
label="名称"
placeholder="必填;不可与其他名称重名"
hint="下载器的别名"
:label="t('downloader.name')"
:placeholder="t('downloader.nameRequired')"
:hint="t('downloader.name')"
persistent-hint
active
/>
@@ -284,9 +292,9 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.host"
label="地址"
:label="t('downloader.host')"
placeholder="http(s)://ip:port"
hint="服务端地址格式http(s)://ip:port"
:hint="t('downloader.host')"
persistent-hint
active
/>
@@ -294,8 +302,8 @@ onUnmounted(() => {
<VCol cols="12" md="6">
<VTextField
v-model="downloaderInfo.config.username"
label="用户名"
hint="登录使用的用户名"
:label="t('downloader.username')"
:hint="t('downloader.username')"
persistent-hint
active
/>
@@ -304,8 +312,8 @@ onUnmounted(() => {
<VTextField
v-model="downloaderInfo.config.password"
type="password"
label="密码"
hint="登录使用的密码"
:label="t('downloader.password')"
:hint="t('downloader.password')"
persistent-hint
active
/>
@@ -315,7 +323,7 @@ onUnmounted(() => {
</VCardText>
<VCardActions class="pt-3">
<VBtn @click="saveDownloaderInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
确定
{{ t('common.save') }}
</VBtn>
</VCardActions>
</VCard>

View File

@@ -2,6 +2,10 @@
import { innerFilterRules } from '@/api/constants'
import { CustomRule } from '@/api/types'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
// 获取i18n实例
const { t } = useI18n()
// 输入参数
const props = defineProps({
@@ -49,7 +53,7 @@ onMounted(() => {
</span>
<VDialogCloseBtn @click="onClose" />
<VCardItem>
<VCardTitle>优先级 {{ props.pri }}</VCardTitle>
<VCardTitle>{{ t('filterRule.priority') }} {{ props.pri }}</VCardTitle>
<VRow>
<VCol>
<VSelect
@@ -57,7 +61,7 @@ onMounted(() => {
variant="underlined"
:items="selectFilterOptions"
chips
label=""
:label="t('filterRule.rules')"
multiple
clearable
@update:modelValue="filtersChanged"

View File

@@ -7,6 +7,10 @@ import { useToast } from 'vue-toast-notification'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
import filter_group_svg from '@images/svg/filter-group.svg'
import { cloneDeep } from 'lodash-es'
import { useI18n } from 'vue-i18n'
// 获取i18n实例
const { t } = useI18n()
// 输入参数
const props = defineProps({
@@ -56,14 +60,14 @@ const groupInfo = ref<FilterRuleGroup>({
// 媒体类型字典
const mediaTypeItems = [
{ title: '通用', value: '' },
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
{ title: t('common.all'), value: '' },
{ title: t('mediaType.movie'), value: '电影' },
{ title: t('mediaType.tv'), value: '电视剧' },
]
// 根据选中的媒体类型,获取对应的媒体类别
const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }]
const default_value = [{ title: t('common.all'), value: '' }]
if (!props.categories || !groupInfo.value.media_type || !props.categories[groupInfo.value.media_type]) {
return default_value
}
@@ -72,11 +76,6 @@ const getCategories = computed(() => {
// 规则组规则卡片列表
const filterRuleCards = ref<FilterCard[]>([])
// 规则组类型,仅用于导入判断
const filterRuleCardsType = ref<FilterCard>({
pri: '',
rules: [],
})
// 导入代码弹窗
const importCodeDialog = ref(false)
@@ -112,10 +111,10 @@ async function shareRules() {
try {
let success
success = copyToClipboard(value)
if (await success) $toast.success('优先级规则已复制到剪贴板!')
else $toast.error('优先级规则复制失败:可能是浏览器不支持或被用户阻止!')
if (await success) $toast.success(t('filterRule.shareSuccess'))
else $toast.error(t('filterRule.shareFailed'))
} catch (error) {
$toast.error('优先级规则复制失败!')
$toast.error(t('filterRule.shareFailed'))
console.error(error)
}
}
@@ -143,7 +142,7 @@ function saveCodeString(type: string, code: any) {
}))
}
} catch (error) {
$toast.error('导入失败!')
$toast.error(t('filterRule.importFailed'))
console.error(error)
}
}
@@ -177,11 +176,11 @@ function opengroupInfoDialog() {
// 保存详情数据
function saveGroupInfo() {
if (!groupInfo.value.name.trim()) {
$toast.error('规则组名称不能为空')
$toast.error(t('filterRule.nameRequired'))
return
}
if (props.groups.some(item => item.name === groupInfo.value.name && item !== props.group)) {
$toast.error(`规则组名称【${groupInfo.value.name}】已存在,请替换`)
$toast.error(t('filterRule.nameDuplicate'))
return
}
@@ -213,7 +212,7 @@ function onClose() {
<div class="align-self-start">
<h5 class="text-h6 mb-1">{{ props.group.name }}</h5>
<div class="text-body-1 mb-3">
<span v-if="!props.group.category">{{ props.group.media_type || '通用' }}</span>
<span v-if="!props.group.category">{{ props.group.media_type || t('common.all') }}</span>
<span v-else>{{ props.group.category }}</span>
</div>
</div>
@@ -221,7 +220,7 @@ function onClose() {
</VCardText>
</VCard>
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
<VCard :title="`${props.group.name} - 配置`" class="rounded-t">
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`" class="rounded-t">
<VDialogCloseBtn v-model="groupInfoDialog" />
<VDivider />
<VCardItem class="pt-1">
@@ -229,9 +228,9 @@ function onClose() {
<VCol cols="12" md="6">
<VTextField
v-model="groupInfo.name"
label="规则组名称"
placeholder="必填;不可与其他规则组重名"
hint="自定义规则组名称"
:label="t('filterRule.name')"
:placeholder="t('filterRule.nameRequired')"
:hint="t('filterRule.name')"
persistent-hint
active
/>
@@ -239,9 +238,9 @@ function onClose() {
<VCol cols="6" md="3">
<VSelect
v-model="groupInfo.media_type"
label="适用媒体类型"
:label="t('filterRule.mediaType')"
:items="mediaTypeItems"
hint="选择规则组适用的媒体类型"
:hint="t('filterRule.mediaType')"
persistent-hint
active
/>
@@ -250,8 +249,8 @@ function onClose() {
<VSelect
v-model="groupInfo.category"
:items="getCategories"
label="适用媒体类别"
hint="选择规则组适用的媒体类别"
:label="t('filterRule.category')"
:hint="t('filterRule.category')"
persistent-hint
active
/>
@@ -278,7 +277,7 @@ function onClose() {
/>
</template>
</draggable>
<div class="text-center" v-if="filterRuleCards.length == 0">请添加或导入规则</div>
<div class="text-center" v-if="filterRuleCards.length == 0">{{ t('filterRule.add') }}</div>
</VCardText>
<VCardActions class="pt-3">
<VBtn color="primary" variant="tonal" @click="addFilterCard">
@@ -291,14 +290,16 @@ function onClose() {
<VIcon icon="mdi-share" />
</VBtn>
<VSpacer />
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 确定 </VBtn>
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<ImportCodeDialog
v-if="importCodeDialog"
v-model="importCodeDialog"
title="导入规则优先级"
:title="t('filterRule.import')"
:dataType="importCodeType"
@close="importCodeDialog = false"
@save="saveCodeString"

View File

@@ -1654,4 +1654,48 @@ export default {
confirm: 'Confirm',
},
},
downloader: {
title: 'Downloader',
name: 'Name',
type: 'Type',
enabled: 'Enabled',
default: 'Default',
host: 'Host',
username: 'Username',
password: 'Password',
category: 'Auto Category Management',
sequentail: 'Sequential Download',
force_resume: 'Force Resume',
first_last_piece: 'First/Last Piece Priority',
saveSuccess: 'Downloader settings saved successfully',
saveFailed: 'Failed to save downloader settings',
nameRequired: 'Name cannot be empty',
nameDuplicate: 'Name already exists',
defaultChanged: 'Default downloader exists, has been replaced',
},
filterRule: {
title: 'Filter Rule',
name: 'Name',
priority: 'Priority',
rules: 'Rules',
add: 'Add Rule',
import: 'Import Rules',
share: 'Share Rules',
save: 'Save Rules',
nameRequired: 'Rule group name cannot be empty',
nameDuplicate: 'Rule group name already exists',
importSuccess: 'Rules imported successfully',
importFailed: 'Failed to import rules',
shareSuccess: 'Rules copied to clipboard',
shareFailed: 'Failed to copy rules',
mediaType: 'Media Type',
category: 'Media Category',
mediaTypeItems: {
movie: 'Movie',
tv: 'TV Show',
anime: 'Anime',
collection: 'Collection',
unknown: 'Unknown',
},
},
}

View File

@@ -1632,4 +1632,40 @@ export default {
confirm: '确定',
},
},
downloader: {
title: '下载器',
name: '名称',
type: '类型',
enabled: '启用',
default: '默认',
host: '地址',
username: '用户名',
password: '密码',
category: '自动分类管理',
sequentail: '顺序下载',
force_resume: '强制继续',
first_last_piece: '优先首尾文件',
saveSuccess: '下载器设置保存成功',
saveFailed: '下载器设置保存失败',
nameRequired: '名称不能为空',
nameDuplicate: '名称已存在',
defaultChanged: '存在默认下载器,已替换',
},
filterRule: {
title: '过滤规则',
priority: '优先级',
rules: '规则',
add: '添加规则',
import: '导入规则',
share: '分享规则',
save: '保存规则',
nameRequired: '规则组名称不能为空',
nameDuplicate: '规则组名称已存在',
importSuccess: '规则导入成功',
importFailed: '规则导入失败',
shareSuccess: '规则已复制到剪贴板',
shareFailed: '规则复制失败',
mediaType: '媒体类型',
category: '媒体类别',
},
}

View File

@@ -1629,4 +1629,38 @@ export default {
confirm: '確定',
},
},
downloader: {
title: '下載器',
name: '名稱',
type: '類型',
enabled: '啟用',
default: '預設',
host: '地址',
username: '用戶名',
password: '密碼',
category: '自動分類管理',
sequentail: '順序下載',
force_resume: '強制繼續',
first_last_piece: '優先首尾文件',
saveSuccess: '下載器設置保存成功',
saveFailed: '下載器設置保存失敗',
nameRequired: '名稱不能為空',
nameDuplicate: '名稱已存在',
defaultChanged: '存在預設下載器,已替換',
},
filterRule: {
title: '過濾規則',
priority: '優先級',
rules: '規則',
add: '添加規則',
import: '導入規則',
share: '分享規則',
save: '保存規則',
nameRequired: '規則組名稱不能為空',
nameDuplicate: '規則組名稱已存在',
importSuccess: '規則導入成功',
importFailed: '規則導入失敗',
shareSuccess: '規則已複製到剪貼板',
shareFailed: '規則複製失敗',
},
}