更新国际化支持

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

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>