更新国际化支持

This commit is contained in:
jxxghp
2025-04-28 13:23:51 +08:00
parent 6b49464059
commit 8cf4b612d5
29 changed files with 3175 additions and 17187 deletions

7
auto-imports.d.ts vendored
View File

@@ -25,6 +25,7 @@ declare global {
const createPinia: typeof import('pinia')['createPinia']
const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
@@ -159,6 +160,7 @@ declare global {
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
@@ -326,7 +328,7 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
@@ -354,6 +356,7 @@ declare module 'vue' {
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
@@ -488,6 +491,7 @@ declare module 'vue' {
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
@@ -527,6 +531,7 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>

1
components.d.ts vendored
View File

@@ -2,6 +2,7 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */

14609
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
"name": "moviepilot",
"version": "2.4.3",
"private": true,
"type": "module",
"bin": "dist/service.js",
"scripts": {
"dev": "vite --host",

View File

@@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},

View File

@@ -12,6 +12,14 @@
*/
import { promises as fs } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
// Get current directory
const __dirname = dirname(fileURLToPath(import.meta.url))
// Create require function for importing JSON files in ESM
const require = createRequire(import.meta.url)
// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
import {

View File

@@ -1,17 +1,17 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"module": "Node16",
"declaration": false,
"declarationMap": false,
"sourceMap": false,
"composite": false,
"strict": true,
"moduleResolution": "node",
"moduleResolution": "node16",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
},
"exclude": [
"./*.js"
]
}
}

View File

@@ -1 +1 @@
{"root":["./build-icons.ts"],"version":"5.7.3"}
{"root":["./build-icons.ts"],"version":"5.8.3"}

View File

@@ -6,7 +6,7 @@ import api from '@/api'
import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n'
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
import { SupportedLocale } from './locales/types'
import { SupportedLocale } from '@/types/i18n'
// 国际化
const { t } = useI18n()

View File

@@ -5,6 +5,10 @@ import FileNavigator from './filebrowser/FileNavigator.vue'
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
import { storageOptions } from '@/api/constants'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 输入参数
const props = defineProps({

View File

@@ -3,6 +3,10 @@ import type { TransferDirectoryConf } from '@/api/types'
import api from '@/api'
import { nextTick } from 'vue'
import { storageOptions } from '@/api/constants'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 输入参数
const props = defineProps({
@@ -23,11 +27,11 @@ const props = defineProps({
const isCollapsed = ref(true)
// 类型下拉字典
const typeItems = [
{ title: '全部', value: '' },
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
]
const typeItems = computed(() => [
{ title: t('common.all'), value: '' },
{ title: t('media.movie'), value: '电影' },
{ title: t('media.tv'), value: '电视剧' },
])
// 计算资源存储字典(整理方式为下载器时不能为远程存储)
const resourceStorageOptions = computed(() => {
@@ -35,18 +39,18 @@ const resourceStorageOptions = computed(() => {
})
// 自动整理方式下拉字典
const transferSourceItems = [
{ title: '不整理', value: '' },
{ title: '下载器监控', value: 'downloader' },
{ title: '目录监控', value: 'monitor' },
{ title: '手动整理', value: 'manual' },
]
const transferSourceItems = computed(() => [
{ title: t('directory.noTransfer'), value: '' },
{ title: t('directory.downloaderMonitor'), value: 'downloader' },
{ title: t('directory.directoryMonitor'), value: 'monitor' },
{ title: t('directory.manualTransfer'), value: 'manual' },
])
// 监控模式下拉字典
const MonitorModeItems = [
{ title: '性能模式', value: 'fast' },
{ title: '兼容模式', value: 'compatibility' },
]
const MonitorModeItems = computed(() => [
{ title: t('directory.performanceMode'), value: 'fast' },
{ title: t('directory.compatibilityMode'), value: 'compatibility' },
])
// 整理方式下拉字典
const transferTypeItems = ref<{ title: string; value: string }[]>([])
@@ -103,23 +107,23 @@ async function loadTransferTypeItems() {
// 整理方式无数据提示
const computedNoDataText = computed(() => {
if (!props.directory.library_storage && !props.directory.storage) {
return '请选择储存'
return t('directory.pleaseSelectStorage')
} else if (!props.directory.library_storage) {
return '请选择媒体库储存'
return t('directory.pleaseSelectLibraryStorage')
} else if (!props.directory.storage) {
return '请选择下载器储存'
return t('directory.pleaseSelectDownloadStorage')
} else {
return '选择的存储类型没有支持的整理方式'
return t('directory.noSupportedTransferType')
}
})
// 覆盖模式下拉字典
const overwriteModeItems = [
{ title: '从不', value: 'never' },
{ title: '总是', value: 'always' },
{ title: '按文件大小', value: 'size' },
{ title: '仅保留最新版本', value: 'latest' },
]
const overwriteModeItems = computed(() => [
{ title: t('directory.never'), value: 'never' },
{ title: t('directory.always'), value: 'always' },
{ title: t('directory.byFileSize'), value: 'size' },
{ title: t('directory.keepLatestOnly'), value: 'latest' },
])
// 定义触发的自定义事件
const emit = defineEmits(['close', 'changed', 'update:modelValue'])
@@ -131,7 +135,7 @@ function onClose() {
// 根据选中的媒体类型,获取对应的媒体类别
const getCategories = computed(() => {
const default_value = [{ title: '全部', value: '' }]
const default_value = [{ title: t('common.all'), value: '' }]
if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value
return default_value.concat(props.categories[props.directory.media_type ?? ''])
})
@@ -180,7 +184,7 @@ watch(
<VTextField
v-model="props.directory.name"
variant="underlined"
label="别名"
:label="t('directory.alias')"
class="me-20 text-high-emphasis font-weight-bold"
/>
<span class="absolute top-3 right-12">
@@ -197,7 +201,7 @@ watch(
v-model="props.directory.media_type"
variant="underlined"
:items="typeItems"
label="媒体类型"
:label="t('directory.mediaType')"
@update:modelValue="props.directory.media_category = ''"
/>
</VCol>
@@ -206,7 +210,7 @@ watch(
v-model="props.directory.media_category"
variant="underlined"
:items="getCategories"
label="媒体类别"
:label="t('directory.mediaCategory')"
/>
</VCol>
<VCol cols="4">
@@ -214,7 +218,7 @@ watch(
v-model="props.directory.storage"
variant="underlined"
:items="resourceStorageOptions"
label="资源存储"
:label="t('directory.resourceStorage')"
/>
</VCol>
<VCol cols="8">
@@ -222,14 +226,17 @@ watch(
v-model="props.directory.download_path"
:storage="props.directory.storage"
variant="underlined"
label="资源目录"
:label="t('directory.resourceDirectory')"
/>
</VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
<VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch>
<VSwitch v-model="props.directory.download_type_folder" :label="t('directory.sortByType')"></VSwitch>
</VCol>
<VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''">
<VSwitch v-model="props.directory.download_category_folder" label="按类别分类"></VSwitch>
<VSwitch
v-model="props.directory.download_category_folder"
:label="t('directory.sortByCategory')"
></VSwitch>
</VCol>
</VRow>
<VDivider v-if="$props.directory.monitor_type" class="my-3 bg-primary" />
@@ -239,7 +246,7 @@ watch(
v-model="props.directory.monitor_type"
variant="underlined"
:items="transferSourceItems"
label="自动整理"
:label="t('directory.autoTransfer')"
/>
</VCol>
</VRow>
@@ -249,7 +256,7 @@ watch(
v-model="props.directory.monitor_mode"
variant="underlined"
:items="MonitorModeItems"
label="监控模式"
:label="t('directory.monitorMode')"
/>
</VCol>
<VCol cols="4">
@@ -257,7 +264,7 @@ watch(
v-model="props.directory.library_storage"
variant="underlined"
:items="storageOptions"
label="媒体库存储"
:label="t('directory.libraryStorage')"
/>
</VCol>
<VCol cols="8">
@@ -265,7 +272,7 @@ watch(
v-model="props.directory.library_path"
:storage="props.directory.library_storage"
variant="underlined"
label="媒体库目录"
:label="t('directory.libraryDirectory')"
/>
</VCol>
<VCol cols="4">
@@ -273,7 +280,7 @@ watch(
v-model="props.directory.transfer_type"
variant="underlined"
:items="transferTypeItems"
label="整理方式"
:label="t('directory.transferType')"
:no-data-text="computedNoDataText"
/>
</VCol>
@@ -282,23 +289,23 @@ watch(
v-model="props.directory.overwrite_mode"
variant="underlined"
:items="overwriteModeItems"
label="覆盖模式"
:label="t('directory.overwriteMode')"
/>
</VCol>
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
<VSwitch v-model="props.directory.library_type_folder" label="按类型分类"></VSwitch>
<VSwitch v-model="props.directory.library_type_folder" :label="t('directory.sortByType')"></VSwitch>
</VCol>
<VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''">
<VSwitch v-model="props.directory.library_category_folder" label="按类别分类"></VSwitch>
<VSwitch v-model="props.directory.library_category_folder" :label="t('directory.sortByCategory')"></VSwitch>
</VCol>
<VCol cols="6">
<VSwitch v-model="props.directory.renaming" label="智能重命名"></VSwitch>
<VSwitch v-model="props.directory.renaming" :label="t('directory.smartRename')"></VSwitch>
</VCol>
<VCol cols="6">
<VSwitch v-model="props.directory.scraping" label="刮削元数据"></VSwitch>
<VSwitch v-model="props.directory.scraping" :label="t('directory.scrapingMetadata')"></VSwitch>
</VCol>
<VCol cols="6">
<VSwitch v-model="props.directory.notify" label="发送通知"></VSwitch>
<VSwitch v-model="props.directory.notify" :label="t('directory.sendNotification')"></VSwitch>
</VCol>
</VRow>
</VForm>

View File

@@ -118,14 +118,14 @@ function getMediaId() {
// 角标颜色
function getChipColor(type: string) {
if (type === t('media.movie')) return 'border-blue-500 bg-blue-600'
else if (type === t('media.tv')) return ' bg-indigo-500 border-indigo-600'
if (type === '电影') return 'border-blue-500 bg-blue-600'
else if (type === '电视剧') return ' bg-indigo-500 border-indigo-600'
else return 'border-purple-600 bg-purple-600'
}
// 添加订阅处理
async function handleAddSubscribe() {
if (props.media?.type === t('media.tv')) {
if (props.media?.type === '电视剧') {
// 弹出季选择列表,支持多选
seasonsSelected.value = []
subscribeSeasonDialog.value = true
@@ -141,7 +141,7 @@ async function addSubscribe(season: number = 0, best_version: number = 0) {
startNProgress()
try {
// 是否洗版
if (!best_version && props.media?.type == t('media.movie')) best_version = isExists.value ? 1 : 0
if (!best_version && props.media?.type == '电影') best_version = isExists.value ? 1 : 0
// 请求API
const result: { [key: string]: any } = await api.post('subscribe/', {
name: props.media?.title,
@@ -279,7 +279,7 @@ async function queryDefaultSubscribeConfig() {
if (!userStore.superUser) return false
try {
let subscribe_config_url = ''
if (props.media?.type === t('media.movie')) subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value) return result.data.value.show_edit_dialog

View File

@@ -10,6 +10,10 @@ import api from '@/api'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import { useDisplay } from 'vuetify'
import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -687,25 +691,31 @@ onMounted(() => {
</VCard>
<!-- 重命名弹窗 -->
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem">
<VCard title="重命名">
<VCard :title="t('file.rename')">
<VDialogCloseBtn @click="renamePopper = false" />
<VDivider />
<VCardText>
<VRow>
<VCol cols="12">
<VTextField v-model="newName" label="新名称" :loading="renameLoading" />
</VCol>
<VCol cols="12" v-if="currentItem && currentItem.type == 'dir'">
<VSwitch v-model="renameAll" label="自动重命名目录内所有媒体文件" />
</VCol>
</VRow>
<div class="mb-3">
<span>{{ t('file.currentName') }}: {{ currentItem?.name }}</span>
</div>
<VTextField v-model="newName" :label="t('file.newName')" />
<VCheckbox
v-if="false && currentItem?.type == 'dir'"
v-model="renameAll"
:label="t('file.includeSubfolders')"
></VCheckbox>
</VCardText>
<VCardActions>
<VBtn color="success" variant="elevated" @click="get_recommend_name" prepend-icon="mdi-magic" class="px-5 me-3">
自动识别名称
</VBtn>
<VBtn :disabled="!newName" variant="elevated" @click="rename" prepend-icon="mdi-check" class="px-5 me-3">
确定
<div class="flex-grow-1" />
<VBtn
:disabled="!newName"
variant="elevated"
:loading="renameLoading"
@click="rename"
prepend-icon="mdi-check"
class="px-5 me-3"
>
{{ t('common.save') }}
</VBtn>
</VCardActions>
</VCard>

View File

@@ -3,6 +3,10 @@ import type { PropType } from 'vue'
import type { FileItem } from '@/api/types'
import { useDisplay } from 'vuetify'
import type { AxiosRequestConfig } from 'axios'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -276,7 +280,7 @@ function getIndentLevel(path: string, ancestorPath: string) {
>
<div class="folder-content">
<VIcon icon="mdi-home" class="me-2" color="primary" />
<span>根目录</span>
<span>{{ t('file.rootDirectory') }}</span>
</div>
</div>

View File

@@ -2,6 +2,10 @@
import type { AxiosRequestConfig } from 'axios'
import type { EndPoints, FileItem } from '@/api/types'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -167,16 +171,16 @@ const sortIcon = computed(() => {
<VIcon v-bind="props" icon="mdi-folder-plus-outline" />
</IconBtn>
</template>
<VCard title="新建文件夹">
<VCard :title="t('file.newFolder')">
<VDialogCloseBtn @click="newFolderPopper = false" />
<VDivider />
<VCardText>
<VTextField v-model="newFolderName" label="名称" />
<VTextField v-model="newFolderName" :label="t('common.name')" />
</VCardText>
<VCardActions>
<div class="flex-grow-1" />
<VBtn :disabled="!newFolderName" variant="elevated" @click="mkdir" prepend-icon="mdi-check" class="px-5 me-3">
新建
{{ t('common.create') }}
</VBtn>
</VCardActions>
</VCard>

View File

@@ -14,14 +14,18 @@ export default {
inputMessage: 'Enter message or command',
send: 'Send',
noData: 'No Data',
noContent: 'No Content Found',
noContent: 'No relevant content found',
all: 'All',
default: 'Default',
name: 'Name',
create: 'Create',
saving: 'Saving',
reset: 'Reset',
},
theme: {
light: 'Light',
dark: 'Dark',
auto: 'Auto',
auto: 'Follow System',
transparent: 'Transparent',
purple: 'Purple',
custom: 'Custom Theme',
@@ -36,22 +40,22 @@ export default {
selectLanguage: 'Select Language',
logout: 'Logout',
restarting: 'Restarting...',
confirmRestart: 'Confirm Restart?',
restartTip: 'After restarting, you will be logged out and need to log in again.',
confirmRestart: 'Confirm restart system?',
restartTip: 'After restart, you will be logged out and need to log in again.',
},
login: {
wallpapers: 'wallpapers',
wallpapers: 'Wallpapers',
username: 'Username',
password: 'Password',
otpCode: 'OTP Code',
otpCode: 'Two-Factor Code',
stayLoggedIn: 'Stay Logged In',
login: 'Login',
networkError: 'Login failed, please check your network connection!',
authFailure: 'Login failed, please check your username, password or OTP code!',
authFailure: 'Login failed, please check your username, password or two-factor authentication!',
permissionDenied: 'Login failed, you do not have permission to access!',
serverError: 'Login failed, server error!',
loginFailed: 'Login Failed',
checkCredentials: 'Please check your username, password or OTP code!',
checkCredentials: 'Please check your username, password or two-factor authentication code!',
},
menu: {
start: 'Start',
@@ -71,7 +75,7 @@ export default {
movieSubscribe: 'Movie Subscription',
tvSubscribe: 'TV Subscription',
history: 'History',
transfer: 'Transfer',
transfer: 'Organize',
rename: 'Rename',
statistic: 'Statistics',
setting: 'Settings',
@@ -84,11 +88,11 @@ export default {
workflow: 'Workflow',
calendar: 'Calendar',
downloadManager: 'Download Manager',
mediaOrganize: 'Media Organizer',
mediaOrganize: 'Media Organize',
fileManager: 'File Manager',
pluginManager: 'Plugins',
siteManager: 'Site Manager',
userManager: 'User Manager',
siteManager: 'Site Management',
userManager: 'User Management',
settings: 'Settings',
},
settingTabs: {
@@ -98,7 +102,7 @@ export default {
},
directory: {
title: 'Storage & Directories',
description: 'Download directories, media library directories, organization, metadata scraping',
description: 'Download directory, media library directory, organization, scraping',
},
site: {
title: 'Sites',
@@ -110,15 +114,15 @@ export default {
},
search: {
title: 'Search & Download',
description: 'Search data sources (TheMovieDb, Douban, Bangumi), download task labels, search sites',
description: 'Search data sources (TheMovieDb, Douban, Bangumi), download task tags, search sites',
},
subscribe: {
title: 'Subscription',
description: 'Subscription sites, subscription modes, subscription rules, upgrade rules',
description: 'Subscription sites, subscription mode, subscription rules, version upgrade rules',
},
scheduler: {
title: 'Services',
description: 'Scheduled tasks',
description: 'Scheduled jobs',
},
notification: {
title: 'Notifications',
@@ -126,7 +130,7 @@ export default {
},
words: {
title: 'Word Lists',
description: 'Custom recognition words, custom groups, custom placeholders, file organization filter words',
description: 'Custom recognition words, custom production/subtitle groups, custom placeholders, file organization block words',
},
about: {
title: 'About',
@@ -136,12 +140,12 @@ export default {
subscribeTabs: {
movie: {
mysub: 'My Subscriptions',
popular: 'Popular',
popular: 'Popular Subscriptions',
},
tv: {
mysub: 'My Subscriptions',
popular: 'Popular',
share: 'Shared',
popular: 'Popular Subscriptions',
share: 'Subscription Shares',
},
},
pluginTabs: {
@@ -155,12 +159,26 @@ export default {
},
user: {
admin: 'Administrator',
normalUser: 'Normal User',
normalUser: 'Regular User',
profile: 'Profile',
systemSettings: 'System Settings',
siteAuth: 'User Authentication',
helpDocs: 'Help Docs',
helpDocs: 'Help Documents',
restart: 'Restart',
management: 'User Management',
noUsers: 'No Users',
clickToAddUser: 'Click Add User card to add users',
addUser: 'Add User',
editUser: 'Edit User',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm Password',
role: 'Role',
email: 'Email',
enabled: 'Enabled',
disabled: 'Disabled',
status: 'Status',
operations: 'Operations',
},
nav: {
more: 'More',
@@ -178,7 +196,7 @@ export default {
},
rule: {
title: 'Rules',
subtitle: 'Rule Test',
subtitle: 'Rule Testing',
},
log: {
title: 'Logs',
@@ -186,7 +204,7 @@ export default {
},
network: {
title: 'Network',
subtitle: 'Speed & Connectivity Test',
subtitle: 'Network Speed and Connectivity Test',
},
system: {
title: 'System',
@@ -202,7 +220,7 @@ export default {
clickToAdd: 'Click to Add',
dragToCanvas: 'Drag to Canvas',
tapComponentHint: 'Tap component to add to canvas',
dragComponentHint: 'Drag components to canvas',
dragComponentHint: 'Drag component to canvas',
},
dashboard: {
storage: 'Storage Space',
@@ -224,9 +242,9 @@ export default {
tv: 'TV Show',
},
subscribe: {
normalSub: 'Subscription',
versionSub: 'Version Upgrade',
addSuccess: '{name} added successfully!',
normalSub: 'Subscribe',
versionSub: 'Version Upgrade Subscribe',
addSuccess: 'Added {name} successfully!',
addFailed: 'Failed to add {name}: {message}!',
cancelSuccess: 'Subscription cancelled!',
cancelFailed: 'Failed to cancel subscription: {message}!',
@@ -234,18 +252,16 @@ export default {
name: 'Name',
searchShares: 'Search Subscription Shares',
keyword: 'Keyword',
noShareData:
'No shared subscription data available, data sharing is not enabled or the server cannot be connected.',
noPopularData:
'No popular subscription data available, data sharing is not enabled or the server cannot be connected.',
noFilterData: 'No matching content found. Please change the filter criteria.',
noShareData: 'No shared subscription data received, data sharing not enabled or server cannot connect.',
noPopularData: 'No popular subscription data received, data sharing not enabled or server cannot connect.',
noFilterData: 'No related content found with current filters, please change filter conditions.',
noSubscribeData: 'Please search to add movie or TV show subscriptions.',
sharer: 'Shared by',
sharer: 'Sharer',
follow: 'Follow',
unfollow: 'Unfollow',
recognitionWords: 'Recognition Words',
cancelShare: 'Cancel Share',
usageCount: 'Used {count} times',
usageCount: '{count} Uses',
},
recommend: {
all: 'All',
@@ -255,7 +271,7 @@ export default {
categoryRankings: 'Rankings',
trendingNow: 'Trending Now',
nowShowing: 'Now Showing',
bangumiDaily: 'Bangumi Daily',
bangumiDaily: 'Bangumi Daily Release',
tmdbHotMovies: 'TMDB Hot Movies',
tmdbHotTVShows: 'TMDB Hot TV Shows',
doubanHotMovies: 'Douban Hot Movies',
@@ -263,13 +279,13 @@ export default {
doubanHotAnime: 'Douban Hot Anime',
doubanNewMovies: 'Douban New Movies',
doubanNewTVShows: 'Douban New TV Shows',
doubanTop250: 'Douban Movie TOP250',
doubanTop250: 'Douban Top 250 Movies',
doubanChineseTVRankings: 'Douban Chinese TV Rankings',
doubanGlobalTVRankings: 'Douban Global TV Rankings',
noCategoryContent: 'No content to display in this category',
configureContent: 'Configure Content',
noCategoryContent: 'No content to display in current category',
configureContent: 'Configure Display Content',
customizeContent: 'Customize Content',
selectContentToDisplay: 'Select the content you want to display',
selectContentToDisplay: 'Select content you want to display on the page',
selectAll: 'Select All',
selectNone: 'Select None',
},
@@ -278,8 +294,8 @@ export default {
dragToReorder: 'Drag to reorder tabs',
},
downloading: {
noDownloader: 'No Downloaders',
configureDownloader: 'Please configure and enable downloaders in settings first.',
noDownloader: 'No Downloader',
configureDownloader: 'Please configure and enable a downloader in settings first.',
},
resource: {
searchResults: 'Resource Search Results',
@@ -301,8 +317,8 @@ export default {
},
notFound: {
title: 'Page Not Found ⚠️',
description: 'The page you are trying to access does not exist. Please check the URL.',
backButton: 'Back',
description: 'The page you tried to access does not exist. Please check if the address is correct.',
backButton: 'Go Back',
},
torrent: {
sortDefault: 'Default',
@@ -310,9 +326,9 @@ export default {
sortSize: 'Size',
sortSeeder: 'Seeders',
filterSite: 'Site',
filterSeason: 'Season',
filterFreeState: 'Promotion',
filterVideoCode: 'Video Code',
filterSeason: 'Season/Episode',
filterFreeState: 'Promotion Status',
filterVideoCode: 'Video Codec',
filterEdition: 'Quality',
filterResolution: 'Resolution',
filterReleaseGroup: 'Release Group',
@@ -325,15 +341,15 @@ export default {
episode: 'Episode {number}',
},
storage: {
usedPercent: 'Used {percent}%',
usedPercent: '{percent}% Used',
},
site: {
noSites: 'No Sites',
sitesWillBeShownHere: 'Added and supported sites will be displayed here.',
sitesWillBeShownHere: 'Added and supported sites will be shown here.',
},
message: {
loadMore: 'Load More',
noMoreData: 'No More Data',
noMoreData: 'No more data',
},
logging: {
level: 'Level',
@@ -833,7 +849,7 @@ export default {
otp: 'Enable Two-Factor Authentication',
avatar: 'Avatar',
uploadAvatar: 'Upload Avatar',
resetDefaultAvatar: 'Reset to Default Avatar',
resetDefaultAvatar: 'Reset Default Avatar',
restoreCurrentAvatar: 'Restore Current Avatar',
notifications: 'Notifications',
wechat: 'WeChat UserID',
@@ -846,28 +862,28 @@ export default {
updatingUser: 'Updating user [{name}], please wait',
usernameRequired: 'Username cannot be empty',
usernameExists: 'Username already exists',
passwordMismatch: 'Passwords do not match',
passwordMismatch: 'The two passwords do not match',
userCreated: 'User [{name}] created successfully',
userCreateFailed: 'Failed to create user: {message}',
userUpdateSuccess: 'User [{name}] updated successfully',
userUpdateFailed: 'Failed to update user: {message}',
userDeleteSuccess: 'User [{name}] deleted successfully',
userDeleteFailed: 'Failed to delete user: {message}',
invalidFile: 'The uploaded file does not meet requirements, please select another avatar',
fileSizeLimit: 'File size cannot exceed 800KB',
invalidFile: 'The uploaded file does not meet the requirements, please choose a new avatar',
fileSizeLimit: 'File size must not exceed 800KB',
avatarUploadSuccess: 'New avatar uploaded successfully, will take effect after saving!',
resetAvatarSuccess: 'Reset to default avatar, will take effect after saving!',
restoreAvatarSuccess: 'Current avatar restored!',
restoreAvatarSuccess: 'Restored current avatar!',
deleteConfirm: 'Confirm delete user [{name}]?',
saveUserInfo: 'Save User Information',
cannotDeleteCurrentUser: 'Cannot delete the currently logged-in user',
cannotDeleteCurrentUser: 'Cannot delete current logged-in user',
deleteUser: 'Delete User',
},
searchBar: {
search: 'Search',
searchPlaceholder: 'Search functions, subscriptions, settings...',
searchPlaceholder: 'Search features, subscriptions, settings...',
recentSearches: 'Recent Searches',
noRecentSearches: 'No recent searches',
noRecentSearches: 'No recent search history',
functions: 'Functions',
noFunctionsFound: 'No matching functions',
plugins: 'Plugins',
@@ -877,13 +893,13 @@ export default {
searchSites: 'Search Sites',
selectSites: 'Select Sites',
collections: 'Collections',
collectionSearch: 'related collections',
actorSearch: 'related actors, directors, etc.',
historySearch: 'related history records',
collectionSearch: 'Related series works',
actorSearch: 'Related actors, directors, etc.',
historySearch: 'Related history records',
siteResources: 'Site Resources',
searchInSites: 'Search for torrents in sites',
relatedResources: 'related resources',
searchTip: 'Search for movies, TV shows, actors, resources, etc.',
searchInSites: 'Search for torrent resources in sites',
relatedResources: 'Related Resources',
searchTip: 'You can search for movies, TV shows, actors, resources, etc.',
},
searchSite: {
selectSites: 'Select Sites',
@@ -903,7 +919,7 @@ export default {
downloader: 'Downloader (Default)',
saveDirectory: 'Save Directory (Auto)',
defaultPlaceholder: 'Leave empty for default',
autoPlaceholder: 'Leave empty for auto match',
autoPlaceholder: 'Leave empty for auto-match',
downloading: 'Downloading...',
startDownload: 'Start Download',
downloadSuccess: '{site} {title} downloaded successfully!',
@@ -914,41 +930,39 @@ export default {
season: 'Season {number}',
title: 'Title',
description: 'Description',
descriptionHint:
'Fill in the description of the subscription. Search terms and identification terms in the subscription will be included in the share by default',
descriptionHint: 'Add a description about this subscription. Search terms, recognition words, etc. will be included in the share by default',
shareUser: 'Share User',
shareUserHint: 'Nickname of the sharer',
shareUserHint: 'Sharer\'s nickname',
confirmShare: 'Confirm Share',
shareSuccess: '{name} shared successfully!',
shareFailed: '{name} share failed: {message}!',
},
u115Auth: {
loginTitle: '115 Cloud Login',
scanQrCode: 'Please scan the QR code with WeChat or 115 client',
scanned: 'QR code scanned, please confirm login',
scanQrCode: 'Please scan with WeChat or 115 client',
scanned: 'Scanned, please confirm login',
complete: 'Complete',
},
aliyunAuth: {
loginTitle: 'Aliyun Drive Login',
scanQrCode: 'Please scan with Aliyun Drive App',
scanned: 'QR code scanned',
scanned: 'Scanned',
complete: 'Complete',
},
rcloneConfig: {
title: 'RClone Configuration',
filePath: 'Rclone Config File Path',
fileContent: 'Rclone Config File Content',
defaultContent:
'# Please fill in the rclone configuration file content here \n# Please refer to https://rclone.org/docs/ \n# Storage node name must be: MP',
filePath: 'rclone config file path',
fileContent: 'rclone config file content',
defaultContent: '# Please fill in your rclone config file content here \n# Please refer to https://rclone.org/docs/ \n# Storage node name must be: MP',
complete: 'Complete',
},
alistConfig: {
title: 'Alist Configuration',
serverUrl: 'Alist Server URL',
serverUrl: 'Alist server address',
username: 'Username',
password: 'Password',
tokenUrl: 'Token URL',
loginType: 'Login Type',
tokenUrl: 'Token acquisition address',
loginType: 'Login method',
loginTypeOptions: {
guest: 'Guest',
username: 'Username & Password',
@@ -967,17 +981,17 @@ export default {
schedule: 'Schedule',
cronExpr: 'Cron Expression',
cronExprDesc: 'Cron expression for workflow scheduling',
nameRequired: 'Please fill in all required information!',
addSuccess: 'Workflow created successfully. Please edit the process!',
addFailed: 'Failed to create workflow: {message}',
editSuccess: 'Workflow modified successfully!',
editFailed: 'Failed to modify workflow: {message}',
nameRequired: 'Please fill in complete information!',
addSuccess: 'Task created successfully, please edit the workflow!',
addFailed: 'Failed to create task: {message}',
editSuccess: 'Task modified successfully!',
editFailed: 'Failed to modify task: {message}',
cancel: 'Cancel',
confirm: 'Confirm',
},
workflowActions: {
title: 'Edit Workflow',
noActionsMessage: 'No actions in the workflow, please add actions',
noActionsMessage: 'Workflow has no actions, please add actions',
addAction: 'Add Action',
editAction: 'Edit Action',
deleteAction: 'Delete Action',
@@ -995,24 +1009,24 @@ export default {
confirmDeleteMessage: 'Are you sure you want to delete this action? This operation cannot be undone.',
yesDelete: 'Yes, Delete',
noCancel: 'Cancel',
invalidConnection: 'Invalid connection: Cannot connect to self or same type port!',
invalidConnection: 'Invalid connection: cannot connect to self or ports of the same type!',
componentNotFound: 'Component {component} not found',
componentAdded: 'Component added to canvas',
saveSuccess: 'Workflow saved successfully!',
saveFailed: 'Failed to save workflow: {message}',
importTitle: 'Import Workflow',
saveSuccess: 'Task workflow saved successfully!',
saveFailed: 'Failed to save task workflow: {message}',
importTitle: 'Import Task Workflow',
importSuccess: 'Import successful!',
importFailed: 'Import failed!',
codeCopied: 'Workflow code copied to clipboard!',
codeCopied: 'Task workflow code copied to clipboard!',
},
siteCookieUpdate: {
title: 'Update Site Cookie',
checkHint: 'Checking login status, please wait...',
confirmUpdateTitle: 'Confirm Update',
confirmUpdateMessage: "Do you want to update this site's cookie with the local cookie?",
confirmUpdateMessage: 'Do you want to update this site\'s cookie with the local cookie?',
processing: 'Processing...',
success: 'Cookie updated successfully',
failed: 'Cookie update failed',
failed: 'Failed to update cookie',
confirm: 'Confirm',
cancel: 'Cancel',
},
@@ -1024,7 +1038,7 @@ export default {
iconLabel: 'Site Icon',
uploadIcon: 'Upload Icon',
cookie: 'Cookie',
rssUrl: 'RSS URL',
rssUrl: 'RSS Link',
enableLabel: 'Enable',
pubEnableLabel: 'Public Resources',
priorityLabel: 'Priority',
@@ -1048,7 +1062,7 @@ export default {
title: 'Plugin Market Settings',
repoUrl: 'Plugin Repository URL',
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
repoHint: 'Multiple addresses separated by commas, only supports Github repositories',
repoHint: 'Multiple URLs separated by commas, only Github repositories are supported',
close: 'Close',
save: 'Save',
saveSuccess: 'Plugin repository saved successfully',
@@ -1057,21 +1071,21 @@ export default {
userAuth: {
title: 'User Authentication',
codeLabel: 'Authentication Code',
codePlaceholder: 'Please enter the authentication code',
authBtn: 'Authenticate',
codePlaceholder: 'Please enter authentication code',
authBtn: 'Start Authentication',
closeBtn: 'Close',
selectSite: 'Select Authentication Site',
selectSiteRequired: 'Please select an authentication site!',
selectSiteRequired: 'Please select authentication site!',
siteConfigNotExist: 'Site configuration does not exist!',
fieldRequired: 'Please enter {name}!',
authSuccess: 'User authentication successful, please login again!',
authSuccess: 'User authentication successful, please log in again!',
authFailed: 'Authentication failed: {message}',
},
transferQueue: {
title: 'Transfer Queue',
title: 'Organization Queue',
name: 'Name',
type: 'Type',
state: 'State',
state: 'Status',
progress: 'Progress',
startTime: 'Start Time',
speedTitle: 'Speed',
@@ -1090,9 +1104,9 @@ export default {
close: 'Close',
},
reorganize: {
title: 'Reorganize',
sourceTitle: 'Source Files',
targetTitle: 'Target Files',
title: 'Organize',
sourceTitle: 'Source File',
targetTitle: 'Target File',
processingTitle: 'Processing',
confirmTitle: 'Confirm',
selectFile: 'Select File',
@@ -1105,120 +1119,119 @@ export default {
selectTargetPath: 'Select Target Path',
selectTargetDir: 'Select Target Directory',
selectFileName: 'Select Filename',
confirmMoving: 'Please confirm moving!',
sourceLabel: 'Source:',
targetLabel: 'Target Directory:',
confirmMoving: 'Please confirm move!',
sourceLabel: 'Source file:',
targetLabel: 'Target directory:',
filenameLabel: 'Filename:',
close: 'Close',
next: 'Next',
previous: 'Previous',
confirm: 'Confirm',
manualTitle: 'Manual Reorganize',
multipleItemsTitle: 'Reorganize - {count} Items',
singleItemTitle: 'Reorganize - {path}',
manualTitle: 'Manual Organization',
multipleItemsTitle: 'Organize - {count} Items',
singleItemTitle: 'Organize - {path}',
targetStorage: 'Target Storage',
targetStorageHint: 'Storage for reorganization',
transferType: 'Transfer Method',
transferTypeHint: 'File operation method',
targetStorageHint: 'Organization target storage',
transferType: 'Organization Method',
transferTypeHint: 'File operation organization method',
targetPath: 'Target Path',
targetPathHint: 'Target path for reorganization, leave empty for automatic matching',
targetPathPlaceholder: 'Leave empty for automatic',
targetPathHint: 'Organization target path, leave empty for auto-match',
targetPathPlaceholder: 'Leave empty for auto',
mediaType: 'Type',
mediaTypeHint: 'Media type of the file',
mediaTypeHint: 'File media type',
tmdbId: 'TheMovieDb ID',
doubanId: 'Douban ID',
mediaIdHint: 'Search media ID by name, leave empty for automatic recognition',
mediaIdPlaceholder: 'Leave empty for automatic recognition',
mediaIdHint: 'Query media ID by name, leave empty for auto recognition',
mediaIdPlaceholder: 'Leave empty for auto recognition',
episodeGroup: 'Episode Group ID',
episodeGroupHint: 'Specify episode group',
episodeGroupPlaceholder: 'Manually search episode group',
episodeGroupPlaceholder: 'Manually query episode group',
season: 'Season',
seasonHint: 'Which season',
episodeDetail: 'Episode',
episodeDetailHint: 'Episode number or range, e.g., 1 or 1,2',
episodeDetailPlaceholder: 'Start episode,end episode',
episodeFormat: 'Episode Format',
episodeFormatHint: 'Use {ep} to locate the episode part in the filename for recognition',
episodeFormatPlaceholder: 'Use {ep} to locate episode',
episodeDetailHint: 'Episode number or range, e.g. 1 or 1,2',
episodeDetailPlaceholder: 'Start episode,End episode',
episodeFormat: 'Episode Positioning',
episodeFormatHint: 'Use {ep} to position episode number part in filename to assist recognition',
episodeFormatPlaceholder: 'Use {ep} to position episode',
episodeOffset: 'Episode Offset',
episodeOffsetHint: 'Episode offset calculation, e.g., -10 or EP*2',
episodeOffsetPlaceholder: 'e.g., -10',
episodeOffsetHint: 'Episode offset calculation, e.g. -10 or EP*2',
episodeOffsetPlaceholder: 'e.g. -10',
episodePart: 'Specify Part',
episodePartHint: 'Specify part, e.g., part1',
episodePartPlaceholder: 'e.g., part1',
minFileSize: 'Minimum File Size (MB)',
minFileSizeHint: 'Only reorganize files larger than the minimum file size',
typeFolderOption: 'Categorize by Type',
typeFolderHint: 'Add subdirectories by media type in the target path during reorganization',
categoryFolderOption: 'Categorize by Category',
categoryFolderHint: 'Add subdirectories by media category in the target path during reorganization',
episodePartHint: 'Specify part, e.g. part1',
episodePartPlaceholder: 'e.g. part1',
minFileSize: 'Min File Size (MB)',
minFileSizeHint: 'Only organize files larger than minimum file size',
typeFolderOption: 'Classify by Type',
typeFolderHint: 'Add subdirectory by media type in target path during organization',
categoryFolderOption: 'Classify by Category',
categoryFolderHint: 'Add subdirectory by media category in target path during organization',
scrapeOption: 'Scrape Metadata',
scrapeHint: 'Automatically scrape metadata after reorganization',
scrapeHint: 'Automatically scrape metadata after organization',
fromHistoryOption: 'Reuse Historical Recognition Info',
fromHistoryHint: 'Use media information recognized in historical reorganization records',
addToQueue: 'Add to Queue',
reorganizeNow: 'Reorganize Now',
fromHistoryHint: 'Use media info already recognized in historical organization records',
addToQueue: 'Add to Organization Queue',
reorganizeNow: 'Organize Now',
auto: 'Auto',
processing: 'Processing ...',
successMessage: 'File {name} has been added to the reorganization queue!',
successMessage: 'File {name} has been added to the organization queue!',
},
subscribeEdit: {
titleDefault: 'Default Subscription Rule',
titleDefault: 'Default Subscription Rules',
titleEditFormat: 'Edit Subscription - {name} {season}',
seasonFormat: 'Season {number}',
tabs: {
basic: 'Basic',
advance: 'Advanced',
},
searchKeyword: 'Search Keyword',
searchKeywordHint: 'Keyword used when searching sites',
searchKeyword: 'Search Keywords',
searchKeywordHint: 'Specify keywords used when searching sites',
totalEpisode: 'Total Episodes',
totalEpisodeHint: 'Total number of episodes',
startEpisode: 'Start Episode',
startEpisodeHint: 'Episode number to start subscription',
startEpisodeHint: 'Starting episode number to subscribe',
quality: 'Quality',
qualityHint: 'Resource quality for subscription',
qualityHint: 'Subscription resource quality',
resolution: 'Resolution',
resolutionHint: 'Resource resolution for subscription',
effect: 'Effect',
effectHint: 'Resource effect for subscription',
subscribeSites: 'Subscribe Sites',
subscribeSitesHint: 'Sites to subscribe from, use system settings if none selected',
resolutionHint: 'Subscription resource resolution',
effect: 'Effects',
effectHint: 'Subscription resource effects',
subscribeSites: 'Subscription Sites',
subscribeSitesHint: 'Range of sites for subscription, use system settings if none selected',
downloader: 'Downloader',
downloaderHint: 'Specify which downloader to use for this subscription',
downloaderHint: 'Specify downloader for this subscription',
savePath: 'Save Path',
savePathHint: 'Specify download path for this subscription, leave empty to use default directories',
savePathHint: 'Specify download save path for this subscription, leave empty to use default download directory',
bestVersion: 'Version Upgrade',
bestVersionHint: 'Enable version upgrade based on priority',
searchImdbid: 'Use ImdbID Search',
bestVersionHint: 'Perform version upgrade subscription based on upgrade priorities',
searchImdbid: 'Search Using ImdbID',
searchImdbidHint: 'Use ImdbID for precise resource searching',
showEditDialog: 'Edit More Rules When Subscribing',
showEditDialogHint: 'Show this edit dialog when adding subscription',
showEditDialogHint: 'Show this edit subscription dialog when adding subscription',
include: 'Include (Keywords, Regex)',
includeHint: 'Include rules, supports regex',
includeHint: 'Include rules, supports regular expressions',
exclude: 'Exclude (Keywords, Regex)',
excludeHint: 'Exclude rules, supports regex',
excludeHint: 'Exclude rules, supports regular expressions',
filterGroups: 'Priority Rule Groups',
filterGroupsHint: 'Filter subscription based on selected rule groups',
episodeGroup: 'Episode Group',
episodeGroupHint: 'Recognize and scrape based on specific episode group',
season: 'Season',
seasonHint: 'Specify season for subscription',
filterGroupsHint: 'Filter subscriptions by selected filter rule groups',
episodeGroup: 'Specify Episode Group',
episodeGroupHint: 'Recognize and scrape by specific episode group',
season: 'Specify Season',
seasonHint: 'Specify any season for subscription',
mediaCategory: 'Custom Category',
mediaCategoryHint: 'Specify category name, auto-detect if empty',
mediaCategoryHint: 'Specify category name, leave empty for auto-recognition',
customWords: 'Custom Recognition Words',
customWordsHint: 'Recognition words only for this subscription',
customWordsPlaceholder:
'Filter word\nReplaced word => Replacement\nFront position word <> Back position word >> Episode offset (EP)\nReplaced word => Replacement && Front position word <> Back position word >> Episode offset (EP)\nReplacement format: {[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} to specify TMDBID/Douban ID, where s and e are season and episode (optional)',
customWordsHint: 'Recognition words only used for this subscription',
customWordsPlaceholder: 'Block word\nReplaced word => Replacement word\nPrefix <> Suffix >> Episode offset (EP)\nReplaced word => Replacement word && Prefix <> Suffix >> Episode offset (EP)\nReplacement word supports format: &#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; to directly specify TMDBID/Douban ID recognition, where s, e are season and episode numbers (optional)',
cancelSubscribe: 'Cancel Subscription',
cancelSubscribeConfirm: 'Are you sure you want to cancel this subscription?',
save: 'Save',
cancelSubscribeConfirm: 'Are you sure you want to cancel the subscription?',
},
subscribeFiles: {
title: 'Downloaded Files',
noFilesMessage: 'No files',
close: 'Close',
downloadTab: 'Downloaded Files',
downloadTab: 'Download Files',
libraryTab: 'Media Library Files',
episodeColumn: 'Episode',
torrentColumn: 'Torrent',
@@ -1262,28 +1275,136 @@ export default {
msgCount: 'Unread Messages',
inviteCount: 'Invites',
bonus: 'Bonus Points',
ratio: 'Share Ratio',
ratio: 'Ratio',
joinTime: 'Join Time',
trafficHistory: 'Traffic History',
seedingDistribution: 'Seeding Distribution',
volumeTitle: 'Volume',
countTitle: 'Count: ',
countTitle: 'Count:',
noData: 'None',
refreshing: 'Refreshing site data...',
close: 'Close',
},
siteResource: {
title: 'Site Resources',
searchHint: 'Search Resources',
searchHint: 'Search resources',
close: 'Close',
},
forkSubscribe: {
title: 'Fork Subscription',
selectSubscriber: 'Select Target User',
overwriteExisting: 'Overwrite Existing',
overwriteExistingHint: 'Whether to overwrite if the target user already has this subscription',
title: 'Copy Subscription',
selectSubscriber: 'Select Copy Target',
overwriteExisting: 'Overwrite Existing Subscription',
overwriteExistingHint: 'Whether to overwrite when target user already has this subscription',
confirm: 'Confirm',
cancel: 'Cancel',
},
},
}
file: {
newFolder: 'New Folder',
createFolder: 'Create Folder',
fileName: 'Filename',
fileSize: 'File Size',
fileType: 'File Type',
lastModified: 'Last Modified',
actions: 'Actions',
rename: 'Rename',
delete: 'Delete',
confirmDelete: 'Confirm Delete',
upload: 'Upload',
download: 'Download',
preview: 'Preview',
selectAll: 'Select All',
deselectAll: 'Deselect All',
moveUp: 'Go Up',
sortByName: 'Sort by Name',
sortByTime: 'Sort by Time',
currentName: 'Current Name',
newName: 'New Name',
includeSubfolders: 'Auto rename all media files in directory',
emptyFolder: 'Empty Folder',
noFilesInFolder: 'No files in this folder',
autoRecognize: 'Auto Recognize Name',
directoryTree: 'Directory Tree',
rootDirectory: 'Root Directory',
noDirectories: 'No directories available',
},
person: {
alias: 'Also Known As:',
credits: 'Credits',
biography: 'Biography',
birthday: 'Birthday',
placeOfBirth: 'Place of Birth',
},
error: {
title: 'Error!',
networkError: 'Unable to get media information, please check your network connection.',
serverError: 'Server error, please try again later.',
notFound: 'Requested resource not found.',
},
plugin: {
sort: {
popular: 'Popular',
name: 'Plugin Name',
author: 'Author',
repository: 'Plugin Repository',
latest: 'Latest Release',
},
installingPlugin: 'Installing plugin...',
installing: 'Installing {name} v{version} ...',
installSuccess: 'Plugin {name} installed successfully!',
installFailed: 'Plugin {name} installation failed: {message}',
uninstalling: 'Uninstalling {name} ...',
uninstallSuccess: 'Plugin {name} uninstalled successfully!',
uninstallFailed: 'Plugin {name} uninstallation failed: {message}',
updating: 'Updating {name} to v{version} ...',
updateSuccess: 'Plugin {name} updated successfully!',
updateFailed: 'Plugin {name} update failed: {message}',
noPlugins: 'No plugins installed',
installed: 'Installed',
notInstalled: 'Not Installed',
hasUpdate: 'Update Available',
configuring: 'Configuring',
enable: 'Enable',
disable: 'Disable',
settings: 'Settings',
},
profile: {
personalInfo: 'Personal Information',
uploadNewAvatar: 'Upload New Avatar',
avatarFormatError: 'The uploaded file does not meet requirements, please select a new avatar',
avatarSizeError: 'File size must not exceed 800KB',
avatarUploadSuccess: 'New avatar uploaded successfully, will take effect after saving!',
resetAvatarSuccess: 'Reset to default avatar, will take effect after saving!',
restoreAvatarSuccess: 'Restored current avatar!',
savingInProgress: 'Saving in progress, please wait...',
usernameRequired: 'Username cannot be empty',
passwordMismatch: 'The two passwords do not match',
usernameChangeSuccess: '[{oldName}] renamed to [{newName}], user information saved successfully!',
saveSuccess: 'User information saved successfully!',
saveFailedWithNameChange: '[{oldName}] renamed to [{newName}], information save failed: {message}!',
saveFailed: 'User information save failed: {message}!',
nickname: 'Nickname',
nicknamePlaceholder: 'Display nickname, takes precedence over username',
accountBinding: 'Account Binding',
wechatUser: 'WeChat User',
telegramUser: 'Telegram User',
slackUser: 'Slack User',
vocechatUser: 'VoceChat User',
synologychatUser: 'SynologyChat User',
doubanUser: 'Douban User',
twoFactorAuthentication: 'Two-Factor Authentication',
enableTwoFactor: 'Enable Two-Factor Authentication',
disableTwoFactor: 'Disable Two-Factor Authentication',
otpGenerateFailed: 'Failed to get OTP URI: {message}!',
otpDisableSuccess: 'Two-factor authentication disabled successfully!',
otpDisableFailed: 'Failed to disable OTP: {message}!',
otpCodeRequired: 'Please enter the 6-digit verification code',
otpEnableSuccess: 'Two-factor authentication enabled successfully!',
otpEnableFailed: 'Failed to enable OTP: {message}!',
authenticatorApp: 'Authenticator App',
authenticatorAppDescription: 'Use an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password to scan the QR code. It will generate a 6-digit code for you to enter below.',
secretKeyTip: 'If you\'re having trouble with the QR code, select manual entry in your app and enter the code above.',
enterVerificationCode: 'Enter verification code to confirm enabling two-factor authentication',
avatarFormatTip: 'JPG, PNG, GIF, WEBP formats allowed, maximum size 800KB.',
},
}

View File

@@ -17,6 +17,10 @@ export default {
noContent: '没有找到相关内容',
all: '全部',
default: '默认',
name: '名称',
create: '新建',
saving: '保存中',
reset: '重置',
},
theme: {
light: '浅色',
@@ -161,6 +165,20 @@ export default {
siteAuth: '用户认证',
helpDocs: '帮助文档',
restart: '重启',
management: '用户管理',
noUsers: '没有用户',
clickToAddUser: '点击添加用户卡片添加用户',
addUser: '添加用户',
editUser: '编辑用户',
username: '用户名',
password: '密码',
confirmPassword: '确认密码',
role: '角色',
email: '邮箱',
enabled: '启用',
disabled: '禁用',
status: '状态',
operations: '操作',
},
nav: {
more: '更多',
@@ -1187,7 +1205,7 @@ export default {
customWords: '自定义识别词',
customWordsHint: '只对该订阅使用的识别词',
customWordsPlaceholder:
'屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量EP\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n其中替换词支持格式{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选',
'屏蔽词\n被替换词 => 替换词\n前定位词 <> 后定位词 >> 集偏移量EP\n被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量EP\n其中替换词支持格式&#123; tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx &#125; 直接指定TMDBID/豆瓣ID识别其中s、e为季数和集数可选',
cancelSubscribe: '取消订阅',
save: '保存',
cancelSubscribeConfirm: '是否确认取消订阅?',
@@ -1264,4 +1282,113 @@ export default {
cancel: '取消',
},
},
file: {
newFolder: '新建文件夹',
createFolder: '创建文件夹',
fileName: '文件名',
fileSize: '文件大小',
fileType: '文件类型',
lastModified: '修改时间',
actions: '操作',
rename: '重命名',
delete: '删除',
confirmDelete: '确认删除',
upload: '上传',
download: '下载',
preview: '预览',
selectAll: '全选',
deselectAll: '取消全选',
moveUp: '返回上一级',
sortByName: '按名称排序',
sortByTime: '按时间排序',
currentName: '当前名称',
newName: '新名称',
includeSubfolders: '自动重命名目录内所有媒体文件',
emptyFolder: '空文件夹',
noFilesInFolder: '该文件夹内没有文件',
autoRecognize: '自动识别名称',
directoryTree: '目录树',
rootDirectory: '根目录',
noDirectories: '没有可用的目录',
},
person: {
alias: '别名:',
credits: '参演作品',
biography: '个人简介',
birthday: '出生日期',
placeOfBirth: '出生地',
},
error: {
title: '出错啦!',
networkError: '无法获取到媒体信息,请检查网络连接。',
serverError: '服务器错误,请稍后重试。',
notFound: '找不到请求的资源。',
},
plugin: {
sort: {
popular: '热门',
name: '插件名称',
author: '作者',
repository: '插件仓库',
latest: '最新发布',
},
installingPlugin: '正在安装插件...',
installing: '正在安装 {name} v{version} ...',
installSuccess: '插件 {name} 安装成功!',
installFailed: '插件 {name} 安装失败:{message}',
uninstalling: '正在卸载 {name} ...',
uninstallSuccess: '插件 {name} 卸载成功!',
uninstallFailed: '插件 {name} 卸载失败:{message}',
updating: '正在更新 {name} 至 v{version} ...',
updateSuccess: '插件 {name} 更新成功!',
updateFailed: '插件 {name} 更新失败:{message}',
noPlugins: '没有安装插件',
installed: '已安装',
notInstalled: '未安装',
hasUpdate: '有更新',
configuring: '配置',
enable: '启用',
disable: '禁用',
settings: '设置',
},
profile: {
personalInfo: '个人信息',
uploadNewAvatar: '上传新头像',
avatarFormatError: '上传的文件不符合要求,请重新选择头像',
avatarSizeError: '文件大小不得大于800KB',
avatarUploadSuccess: '新头像上传成功,待保存后生效!',
resetAvatarSuccess: '已重置为默认头像,待保存后生效!',
restoreAvatarSuccess: '已还原当前使用头像!',
savingInProgress: '正在保存中,请稍后...',
usernameRequired: '用户名不能为空',
passwordMismatch: '两次输入的密码不一致',
usernameChangeSuccess: '【{oldName}】更名【{newName}】,用户信息保存成功!',
saveSuccess: '用户信息保存成功!',
saveFailedWithNameChange: '【{oldName}】更名【{newName}】,信息保存失败:{message}',
saveFailed: '用户信息保存失败:{message}',
nickname: '昵称',
nicknamePlaceholder: '显示昵称,优先于用户名显示',
accountBinding: '账号绑定',
wechatUser: '微信用户',
telegramUser: 'Telegram用户',
slackUser: 'Slack用户',
vocechatUser: 'VoceChat用户',
synologychatUser: 'SynologyChat用户',
doubanUser: '豆瓣用户',
twoFactorAuthentication: '登录双重验证',
enableTwoFactor: '开启双重验证',
disableTwoFactor: '关闭双重验证',
otpGenerateFailed: '获取otp uri失败{message}',
otpDisableSuccess: '关闭登录双重验证成功!',
otpDisableFailed: '关闭otp失败{message}',
otpCodeRequired: '请填写6位验证码',
otpEnableSuccess: '开启登录双重验证成功!',
otpEnableFailed: '开启otp失败{message}',
authenticatorApp: '身份验证器',
authenticatorAppDescription:
'使用像Google Authenticator、Microsoft Authenticator、Authy或1Password这样的身份验证器应用程序扫描二维码。它将为您生成一个6位数的代码供您在下方输入。',
secretKeyTip: '如果您在使用二维码时遇到困难,请在您的应用程序中选择手动输入以上代码。',
enterVerificationCode: '输入验证码以确认开启双重验证',
avatarFormatTip: '允许 JPG、PNG、GIF、WEBP 格式, 最大尺寸 800KB。',
},
}

View File

@@ -1,31 +1,24 @@
<script setup lang="ts">
import PersonCardListView from '@/views/discover/PersonCardListView.vue'
// 输入参数
const props = defineProps({
// API路径
paths: Array as PropType<string[]> | PropType<string>,
})
// 路由参数
const route = useRoute()
// 标题
const id = route.query?.id?.toString()
const title = route.query?.title?.toString()
// 类型
const source = route.query?.source?.toString()
const type = route.query?.type?.toString()
// 计算API路径
function getApiPath(paths: string[] | string) {
if (Array.isArray(paths)) return paths.join('/')
else return paths
}
const apipath = route.query?.apipath?.toString()
</script>
<template>
<div>
<VPageContentTitle :title="title" />
<PersonCardListView :apipath="getApiPath(props.paths || '')" :params="route.query" :type="type" />
<PersonCardListView
:credits-id="id"
:credits-name="title"
:credits-source="source"
:credits-type="type"
:credits-apipath="apipath"
/>
</div>
</template>

View File

@@ -3,5 +3,7 @@ import TransferHistoryView from '@/views/reorganize/TransferHistoryView.vue'
</script>
<template>
<TransferHistoryView />
<div>
<TransferHistoryView />
</div>
</template>

View File

@@ -1,15 +1,25 @@
<script setup lang="ts">
import MediaDetailView from '@/views/discover/MediaDetailView.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 路由参数
const route = useRoute()
// TMDBID
// TMDB ID
const mediaid = route.query?.mediaid?.toString()
// 类型
// 类型:电影、电视剧
const type = route.query?.type?.toString()
// 媒体信息来源TMDB、豆瓣
const source = route.query?.source?.toString() || 'themoviedb'
// TMDB ID
const page = route.query?.page?.toString() || '1'
// 标题
const title = route.query?.title?.toString()
@@ -19,6 +29,6 @@ const year = route.query?.year?.toString()
<template>
<div>
<MediaDetailView :mediaid="mediaid" :type="type" :title="title" :year="year" />
<MediaDetailView :mediaid="mediaid" :type="type" :source="source" :page="page" :title="title" :year="year" />
</div>
</template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import SiteListView from '@/views/site/SiteCardListView.vue'
import SiteCardListView from '@/views/site/SiteCardListView.vue'
</script>
<template>
<div>
<SiteListView />
<SiteCardListView />
</div>
</template>

View File

@@ -3,5 +3,7 @@ import WorkflowListView from '@/views/workflow/WorkflowListView.vue'
</script>
<template>
<WorkflowListView />
<div>
<WorkflowListView />
</div>
</template>

View File

@@ -4,6 +4,10 @@ import api from '@/api'
import personIcon from '@images/misc/person.png'
import type { Person } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 输入参数
const personProps = defineProps({
@@ -67,7 +71,7 @@ function getPersonImage() {
function getAlsoKnownAs() {
if (!personDetail.value?.also_known_as) return ''
if (personProps.source === 'themoviedb') {
return '别名:' + personDetail.value.also_known_as.join('、')
return t('person.alias') + personDetail.value.also_known_as.join('、')
} else {
return personDetail.value.also_known_as.join('')
}
@@ -81,7 +85,7 @@ function getPersonCreditsPath() {
} else if (personProps.source === 'bangumi') {
apipath = 'bangumi'
}
return `/browse/${apipath}/person/credits/${personDetail.value.id}?title=参演作品`
return `/browse/${apipath}/person/credits/${personDetail.value.id}?title=${t('person.credits')}`
}
// 参演作品API路径
@@ -136,7 +140,7 @@ onBeforeMount(() => {
<div>
<div class="slider-header">
<RouterLink :to="getPersonCreditsPath()" class="slider-title">
<span>参演作品</span>
<span>{{ t('person.credits') }}</span>
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
</RouterLink>
</div>
@@ -146,7 +150,7 @@ onBeforeMount(() => {
<NoDataFound
v-if="!personDetail.id && isRefreshed"
error-code="500"
error-title="出错啦"
error-description="无法获取到媒体信息请检查网络连接"
:error-title="t('error.title')"
:error-description="t('error.networkError')"
/>
</template>

View File

@@ -12,6 +12,10 @@ import { isNullOrEmptyObject } from '@/@core/utils'
import { getPluginTabs } from '@/router/i18n-menu'
import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
const route = useRoute()
@@ -37,13 +41,13 @@ const activeSort = ref(null)
const orderConfig = ref<{ id: string }[]>([])
// 排序选项
const sortOptions = [
{ title: '热门', value: 'count' },
{ title: '插件名称', value: 'plugin_name' },
{ title: '作者', value: 'plugin_author' },
{ title: '插件仓库', value: 'repo_url' },
{ title: '最新发布', value: 'add_time' },
]
const sortOptions = computed(() => [
{ title: t('plugin.sort.popular'), value: 'count' },
{ title: t('plugin.sort.name'), value: 'plugin_name' },
{ title: t('plugin.sort.author'), value: 'plugin_author' },
{ title: t('plugin.sort.repository'), value: 'repo_url' },
{ title: t('plugin.sort.latest'), value: 'add_time' },
])
// 加载中
const loading = ref(false)
@@ -105,7 +109,7 @@ const $toast = useToast()
const progressDialog = ref(false)
// 进度框文本
const progressText = ref('正在安装插件...')
const progressText = ref(t('plugin.installingPlugin'))
// 过滤表单
const filterForm = reactive({
@@ -217,7 +221,7 @@ async function installPlugin(item: Plugin) {
try {
// 显示等待提示框
progressDialog.value = true
progressText.value = `正在安装 ${item?.plugin_name} v${item?.plugin_version} ...`
progressText.value = t('plugin.installing', { name: item?.plugin_name, version: item?.plugin_version })
const result: { [key: string]: any } = await api.get(`plugin/install/${item?.id}`, {
params: {
@@ -230,12 +234,12 @@ async function installPlugin(item: Plugin) {
progressDialog.value = false
if (result.success) {
$toast.success(`插件 ${item?.plugin_name} 安装成功!`)
$toast.success(t('plugin.installSuccess', { name: item?.plugin_name }))
// 刷新
refreshData()
} else {
$toast.error(`插件 ${item?.plugin_name} 安装失败:${result.message}`)
$toast.error(t('plugin.installFailed', { name: item?.plugin_name, message: result.message }))
}
} catch (error) {
console.error(error)

View File

@@ -2,6 +2,10 @@
import api from '@/api'
import { FileItem, StorageConf, TransferDirectoryConf } from '@/api/types'
import FileBrowser from '@/components/FileBrowser.vue'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
const endpoints = {
list: {
@@ -179,7 +183,7 @@ onMounted(() => {
<style lang="scss" scoped>
.file-browser-view {
height: 100%;
position: relative;
block-size: 100%;
}
</style>

View File

@@ -6,6 +6,11 @@ import UserCard from '@/components/cards/UserCard.vue'
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
import { useDisplay } from 'vuetify'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// APP
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
@@ -68,7 +73,7 @@ useDynamicButton({
<template>
<!-- 页面标题 -->
<VPageContentTitle title="用户管理" />
<VPageContentTitle :title="t('user.management')" />
<div class="card-list-container">
<!-- 加载中提示 -->
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
@@ -87,7 +92,7 @@ useDynamicButton({
<!-- 无数据提示 -->
<div v-if="allUsers.length === 0 && isRefreshed">
<NoDataFound error-code="404" error-title="没有用户" error-description="点击添加用户卡片添加用户" />
<NoDataFound error-code="404" :error-title="t('user.noUsers')" :error-description="t('user.clickToAddUser')" />
</div>
<!-- 新增用户按钮 -->

View File

@@ -7,6 +7,10 @@ import type { User } from '@/api/types'
import avatar1 from '@images/avatars/avatar-1.png'
import { useDisplay } from 'vuetify'
import { useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
// 国际化
const { t } = useI18n()
// 显示器宽度
const display = useDisplay()
@@ -73,19 +77,19 @@ function changeAvatar(file: Event) {
const maxSize = 800 * 1024
// 检查文件是否为图片
if (!allowedTypes.includes(selectedFile.type)) {
$toast.error('上传的文件不符合要求,请重新选择头像')
$toast.error(t('profile.avatarFormatError'))
return
}
// 检查文件大小
if (selectedFile.size > maxSize) {
$toast.error('文件大小不得大于800KB')
$toast.error(t('profile.avatarSizeError'))
return
}
fileReader.readAsDataURL(selectedFile)
fileReader.onload = () => {
if (typeof fileReader.result === 'string') {
currentAvatar.value = fileReader.result
$toast.success('新头像上传成功,待保存后生效!')
$toast.success(t('profile.avatarUploadSuccess'))
}
}
}
@@ -94,13 +98,13 @@ function changeAvatar(file: Event) {
// 重置默认头像
function resetDefaultAvatar() {
currentAvatar.value = avatar1
$toast.success('已重置为默认头像,待保存后生效!')
$toast.success(t('profile.resetAvatarSuccess'))
}
// 还原当前头像
function restoreCurrentAvatar() {
currentAvatar.value = accountInfo.value.avatar
$toast.success('已还原当前使用头像!')
$toast.success(t('profile.restoreAvatarSuccess'))
}
// 加载当前用户信息
@@ -121,16 +125,16 @@ async function fetchUserInfo() {
// 保存用户信息
async function saveAccountInfo() {
if (isSaving.value) {
$toast.error('正在保存中,请稍后...')
$toast.error(t('profile.savingInProgress'))
return
}
if (!currentUserName.value) {
$toast.error('用户名不能为空')
$toast.error(t('profile.usernameRequired'))
return
}
if (newPassword.value || confirmPassword.value) {
if (newPassword.value !== confirmPassword.value) {
$toast.error('两次输入的密码不一致')
$toast.error(t('profile.passwordMismatch'))
return
}
accountInfo.value.password = newPassword.value
@@ -157,11 +161,11 @@ async function saveAccountInfo() {
if (result.success) {
if (oldUserName !== currentUserName.value) {
$toast.success(`${oldUserName}】更名【${currentUserName.value}】,用户信息保存成功!`)
$toast.success(t('profile.usernameChangeSuccess', { oldName: oldUserName, newName: currentUserName.value }))
// 更新本地用户名显示
userStore.setUserName(currentUserName.value)
} else {
$toast.success('用户信息保存成功!')
$toast.success(t('profile.saveSuccess'))
}
// 更新本地头像显示
if (oldAvatar !== currentAvatar.value) {
@@ -169,9 +173,15 @@ async function saveAccountInfo() {
}
} else {
if (oldAvatar !== currentAvatar.value) {
$toast.error(`${oldUserName}】更名【${currentUserName.value}】,信息保存失败:${result.message}`)
$toast.error(
t('profile.saveFailedWithNameChange', {
oldName: oldUserName,
newName: currentUserName.value,
message: result.message,
}),
)
} else {
$toast.error(`用户信息保存失败:${result.message}`)
$toast.error(t('profile.saveFailed', { message: result.message }))
}
// 失败缓存值还原
currentUserName.value = accountInfo.value.name
@@ -195,7 +205,7 @@ async function getOtpUri() {
qrCode.value = result.data.uri
otpDialog.value = true
} else {
$toast.error(`获取otp uri失败${result.message}`)
$toast.error(t('profile.otpGenerateFailed', { message: result.message }))
}
} catch (error) {
console.log(error)
@@ -208,9 +218,9 @@ async function disableOtp() {
const result: { [key: string]: any } = await api.post('user/otp/disable')
if (result.success) {
accountInfo.value.is_otp = false
$toast.success('关闭登录双重验证成功!')
$toast.success(t('profile.otpDisableSuccess'))
} else {
$toast.error(`关闭otp失败${result.message}`)
$toast.error(t('profile.otpDisableFailed', { message: result.message }))
}
} catch (error) {
console.log(error)
@@ -220,7 +230,7 @@ async function disableOtp() {
// 启用Otp
async function judgeOtpPassword() {
if (!otpPassword.value) {
$toast.error('请填写6位验证码')
$toast.error(t('profile.otpCodeRequired'))
return
}
try {
@@ -230,11 +240,11 @@ async function judgeOtpPassword() {
})
if (result.success) {
$toast.success('开启登录双重验证成功!')
$toast.success(t('profile.otpEnableSuccess'))
otpDialog.value = false
accountInfo.value.is_otp = true
} else {
$toast.error(`开启otp失败${result.message}`)
$toast.error(t('profile.otpEnableFailed', { message: result.message }))
}
} catch (error) {
console.log(error)
@@ -259,7 +269,7 @@ watch(
<div>
<VRow>
<VCol cols="12">
<VCard title="个人信息">
<VCard :title="t('profile.personalInfo')">
<VCardText class="flex">
<!-- 👉 Avatar -->
<VAvatar rounded="lg" size="100" class="me-6" :image="currentAvatar" />
@@ -269,7 +279,7 @@ watch(
<div class="flex flex-wrap gap-2">
<VBtn color="primary" @click="refInputEl?.click()">
<VIcon icon="mdi-cloud-upload-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">上传新头像</span>
<span v-if="display.mdAndUp.value" class="ms-2">{{ t('profile.uploadNewAvatar') }}</span>
</VBtn>
<input
@@ -283,12 +293,12 @@ watch(
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar">
<VIcon icon="mdi-refresh" />
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
<span v-if="display.mdAndUp.value" class="ms-2">{{ t('common.reset') }}</span>
</VBtn>
<VBtn type="reset" color="error" variant="tonal" @click="resetDefaultAvatar">
<VIcon icon="mdi-image-sync-outline" />
<span v-if="display.mdAndUp.value" class="ms-2">默认</span>
<span v-if="display.mdAndUp.value" class="ms-2">{{ t('common.default') }}</span>
</VBtn>
<VBtn
@@ -298,12 +308,12 @@ watch(
>
<VIcon icon="mdi-account-key" />
<span v-if="display.mdAndUp.value" class="ms-2">{{
accountInfo.is_otp ? '关闭双重验证' : '开启双重验证'
accountInfo.is_otp ? t('profile.disableTwoFactor') : t('profile.enableTwoFactor')
}}</span>
</VBtn>
</div>
<p class="text-body-1 mb-0">允许 JPGPNGGIFWEBP 格式 最大尺寸 800KB</p>
<p class="text-body-1 mb-0">{{ t('profile.avatarFormatTip') }}</p>
</form>
</VCardText>
@@ -312,10 +322,16 @@ watch(
<VForm class="mt-6">
<VRow>
<VCol cols="12" md="6">
<VTextField v-model="currentUserName" density="comfortable" readonly label="用户名" />
<VTextField v-model="currentUserName" density="comfortable" readonly :label="t('user.username')" />
</VCol>
<VCol cols="12" md="6">
<VTextField v-model="accountInfo.email" density="comfortable" clearable label="邮箱" type="email" />
<VTextField
v-model="accountInfo.email"
density="comfortable"
clearable
:label="t('user.email')"
type="email"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
@@ -324,7 +340,7 @@ watch(
:type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable
label="密码"
:label="t('user.password')"
autocomplete=""
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
/>
@@ -337,7 +353,7 @@ watch(
:type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
clearable
label="确认密码"
:label="t('user.confirmPassword')"
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
/>
</VCol>
@@ -346,14 +362,14 @@ watch(
v-model="accountInfo.nickname"
density="comfortable"
clearable
label="昵称"
placeholder="显示昵称,优先于用户名显示"
:label="t('profile.nickname')"
:placeholder="t('profile.nicknamePlaceholder')"
/>
</VCol>
</VRow>
<VDivider class="my-10">
<span>账号绑定</span>
<span>{{ t('profile.accountBinding') }}</span>
</VDivider>
<VRow>
@@ -362,7 +378,7 @@ watch(
v-model="accountInfo.settings.wechat_userid"
density="comfortable"
clearable
label="微信用户"
:label="t('profile.wechatUser')"
/>
</VCol>
<VCol cols="12" md="6">
@@ -370,7 +386,7 @@ watch(
v-model="accountInfo.settings.telegram_userid"
density="comfortable"
clearable
label="Telegram用户"
:label="t('profile.telegramUser')"
/>
</VCol>
<VCol cols="12" md="6">
@@ -378,7 +394,7 @@ watch(
v-model="accountInfo.settings.slack_userid"
density="comfortable"
clearable
label="Slack用户"
:label="t('profile.slackUser')"
/>
</VCol>
<VCol cols="12" md="6">
@@ -386,7 +402,7 @@ watch(
v-model="accountInfo.settings.vocechat_userid"
density="comfortable"
clearable
label="VoceChat用户"
:label="t('profile.vocechatUser')"
/>
</VCol>
<VCol cols="12" md="6">
@@ -394,7 +410,7 @@ watch(
v-model="accountInfo.settings.synologychat_userid"
density="comfortable"
clearable
label="SynologyChat用户"
:label="t('profile.synologychatUser')"
/>
</VCol>
<VCol cols="12" md="6">
@@ -402,7 +418,7 @@ watch(
v-model="accountInfo.settings.douban_userid"
density="comfortable"
clearable
label="豆瓣用户"
:label="t('profile.doubanUser')"
/>
</VCol>
</VRow>
@@ -410,8 +426,8 @@ watch(
<!-- 👉 Form Actions -->
<VCol cols="12" class="d-flex flex-wrap gap-4">
<VBtn @click="saveAccountInfo" :disabled="isSaving">
<span v-if="isSaving">保存中...</span>
<span v-else>保存</span>
<span v-if="isSaving">{{ t('common.saving') }}...</span>
<span v-else>{{ t('common.save') }}</span>
</VBtn>
</VCol>
</VRow>
@@ -427,40 +443,33 @@ watch(
<VCard>
<VDialogCloseBtn @click="otpDialog = false" />
<VCardText>
<h4 class="text-h4 text-center mb-6 mt-5">登录双重验证</h4>
<h5 class="text-h5 font-weight-medium mb-2">身份验证器</h5>
<h4 class="text-h4 text-center mb-6 mt-5">{{ t('profile.twoFactorAuthentication') }}</h4>
<h5 class="text-h5 font-weight-medium mb-2">{{ t('profile.authenticatorApp') }}</h5>
<p class="mb-6">
使用像Google AuthenticatorMicrosoft
AuthenticatorAuthy或1Password这样的身份验证器应用程序扫描二维码它将为您生成一个6位数的代码供您在下方输入
{{ t('profile.authenticatorAppDescription') }}
</p>
<div class="my-6">
<QrcodeVue class="mx-auto" :value="qrCode" :size="200" max-width="25rem" />
</div>
<VAlert
:title="secret"
variant="tonal"
type="warning"
class="my-4"
text="如果您在使用二维码时遇到困难,请在您的应用程序中选择手动输入以上代码。"
>
<VAlert :title="secret" variant="tonal" type="warning" class="my-4" :text="t('profile.secretKeyTip')">
<template #prepend />
</VAlert>
<VForm>
<VTextField
v-model="otpPassword"
type="text"
label="输入验证码以确认开启双重验证"
:label="t('profile.enterVerificationCode')"
autocomplete=""
class="mb-8"
variant="outlined"
/>
<div class="d-flex justify-end flex-wrap gap-4">
<VBtn variant="outlined" color="secondary" @click="otpDialog = false"> 取消 </VBtn>
<VBtn variant="outlined" color="secondary" @click="otpDialog = false"> {{ t('common.cancel') }} </VBtn>
<VBtn @click="judgeOtpPassword">
<template #prepend>
<VIcon icon="mdi-check" />
</template>
确定
{{ t('common.confirm') }}
</VBtn>
</div>
</VForm>

View File

@@ -3,8 +3,8 @@
"baseUrl": "./",
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"module": "Node16",
"moduleResolution": "node16",
"isolatedModules": true,
"strict": true,
"jsx": "preserve",

4736
yarn.lock

File diff suppressed because it is too large Load Diff