Commit adb87977 authored by tyyin lan's avatar tyyin lan

feat: 多语言切换

parent 6f8d5ac9
......@@ -16,4 +16,5 @@
"src/locales/langs"
],
"i18n-ally.sourceLanguage": "zh-cn",
}
"i18n-ally.keystyle": "nested",
}
\ No newline at end of file
......@@ -25,6 +25,7 @@ export default [
ConversationMessageItem: 'readonly',
ConversationMessageItemInfo: 'readonly',
MittEvents: 'readonly',
I18n: 'readonly',
},
parser: vueParser,
parserOptions: {
......
<script setup lang="ts">
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Up, Translate, Down } from '@icon-park/vue-next'
interface Props {
arrowDirection: 'top' | 'bottom'
btnBgColor?: string
}
const { arrowDirection = 'bottom', btnBgColor = '#f4f5f5' } = defineProps<Props>()
const systemLanguageStore = useSystemLanguageStore()
const { locale } = useI18n()
const isShowLanguagePopover = ref(false)
function handleLanguageOptionsUpdateShow(value: boolean) {
if (value) {
isShowLanguagePopover.value = true
} else {
isShowLanguagePopover.value && (isShowLanguagePopover.value = false)
}
}
function handleLanguageOptionSelect(key: I18n.LangType) {
systemLanguageStore.updateCurrentLanguageInfo(key)
locale.value = key
isShowLanguagePopover.value = false
}
</script>
<template>
<n-popover
placement="bottom"
trigger="hover"
class="!p-[10px]"
:show-arrow="false"
:show="isShowLanguagePopover"
@update:show="handleLanguageOptionsUpdateShow"
>
<template #trigger>
<button
class="flex w-full items-center rounded-[6px] bg-[#f4f5f5] px-[12px] py-[6px] transition hover:bg-[#eceded]"
:style="{ backgroundColor: btnBgColor }"
>
<Translate theme="outline" size="16" fill="#000dff" :stroke-width="3" />
<div class="ml-[10px] flex flex-1 items-center justify-between">
<span>{{ systemLanguageStore.currentLanguageInfo.label }}</span>
<Up
v-if="arrowDirection === 'top'"
class="transition-[rotate] duration-300 ease-in-out"
:class="{ 'rotate-180': isShowLanguagePopover }"
theme="outline"
size="21"
fill="#333"
:stroke-width="3"
/>
<Down
v-else
theme="outline"
size="21"
fill="#333"
:stroke-width="3"
class="transition-[rotate] duration-300 ease-in-out"
:class="{ 'rotate-180': isShowLanguagePopover }"
/>
</div>
</button>
</template>
<ul class="select-none">
<li
v-for="langItem in systemLanguageStore.languageOptions"
:key="langItem.key"
class="relative mb-[6px] cursor-pointer bg-[#f3f3f5] px-[20px] py-[4px] text-center transition last:mb-0 hover:bg-[#e7e7e7]"
@click="handleLanguageOptionSelect(langItem.key as I18n.LangType)"
>
{{ langItem.label }}
<i
v-show="langItem.key === systemLanguageStore.currentLanguageInfo.key"
class="iconfont icon-xuanze text-theme-color absolute bottom-0 right-0 text-[14px]"
></i>
</li>
</ul>
</n-popover>
</template>
<script setup lang="ts">
import { h, readonly, ref, shallowReadonly, watchEffect } from 'vue'
import { computed, h, ref, watchEffect } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Plus } from '@icon-park/vue-next'
import type { MenuOption } from 'naive-ui'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { useUserStore } from '@/store/modules/user'
import LanguageSetting from '@/components/language-setting/language-setting.vue'
const { t } = useI18n()
......@@ -18,26 +19,30 @@ const defaultAvatar = 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/17259529
const currentMenuValue = ref('')
const menuOptions = shallowReadonly<MenuOption[]>([
{
label: () => h('div', {}, t('router_title_module.home')),
key: 'Home',
icon: () => h('i', { class: 'iconfont icon-home' }),
},
{
label: () => h('div', {}, t('router_title_module.personal')),
key: 'PersonalSpace',
icon: () => h('i', { class: 'iconfont icon-personal' }),
},
])
const avatarOptions = readonly([
{
label: () => h('div', {}, t('common_module.logout')),
key: 'logout',
icon: () => h(CustomIcon, { icon: 'teenyicons:logout-solid' }),
},
])
const menuOptions = computed<MenuOption[]>(() => {
return [
{
label: () => h('div', {}, t('router_title_module.home')),
key: 'Home',
icon: () => h('i', { class: 'iconfont icon-home' }),
},
{
label: () => h('div', {}, t('router_title_module.personal')),
key: 'PersonalSpace',
icon: () => h('i', { class: 'iconfont icon-personal' }),
},
]
})
const avatarOptions = computed(() => {
return [
{
label: () => h('div', {}, t('common_module.logout')),
key: 'logout',
icon: () => h(CustomIcon, { icon: 'teenyicons:logout-solid' }),
},
]
})
watchEffect(() => {
currentMenuValue.value = (currentRoute.meta.belong as string) || ''
......@@ -98,27 +103,33 @@ function handleMenuValueChange(key: string) {
</n-scrollbar>
</div>
<div class="my-7 px-3">
<NDropdown
v-if="userStore.isLogin"
trigger="click"
placement="top-start"
:options="avatarOptions"
@select="handleDropdownSelect"
>
<div class="flex h-full cursor-pointer items-center">
<NAvatar round :size="40" object-fit="cover" :src="userStore.userInfo.avatarUrl || defaultAvatar" />
<div class="ml-3 line-clamp-1 max-w-[140px] select-none break-all text-base">
{{ userStore.userInfo.nickName || t('common_module.not_login_text') }}
<div class="mb-[20px] mt-6 px-[12px]">
<div>
<NDropdown
v-if="userStore.isLogin"
trigger="click"
placement="top-start"
:options="avatarOptions"
@select="handleDropdownSelect"
>
<div class="flex h-full cursor-pointer items-center">
<NAvatar round :size="40" object-fit="cover" :src="userStore.userInfo.avatarUrl || defaultAvatar" />
<div class="ml-3 line-clamp-1 max-w-[140px] select-none break-all text-base">
{{ userStore.userInfo.nickName || t('common_module.not_login_text') }}
</div>
</div>
</NDropdown>
<div v-else>
<NButton type="primary" class="w-full! rounded-md!" @click="handleToLogin">
{{ t('common_module.login_now') }}
</NButton>
</div>
</NDropdown>
</div>
<div v-else>
<NButton type="primary" class="w-full! rounded-md!" @click="handleToLogin">
{{ t('common_module.login_now') }}
</NButton>
<div class="mt-[10px]">
<LanguageSetting arrow-direction="top" />
</div>
</div>
</div>
......
import { type App } from 'vue'
import { createI18n } from 'vue-i18n'
import messages from './messages'
import { ss } from '@/utils/storage'
export const defaultLocale = 'zh-CN'
const i18n = createI18n<[I18n.Schema], I18n.LangType>({
legacy: false,
locale: 'zh-CN',
locale: ss.get('i18nextLng') || defaultLocale,
fallbackLocale: 'zh-HK',
messages,
})
......
This diff is collapsed.
......@@ -116,7 +116,7 @@ home_module:
agent_welcome_message: 'Hi, 欢迎使用SuperLink'
agent_description: '在这里,你可以体验多个平台的模型和专属的智能体'
currently_in_the_latest_session: '当前已是最新会话'
switching_over: '切换中...'
switching_over: '切换中'
history_application_success: '历史记录应用成功'
history_application_failed_please_try_again: '历史记录应用失败,请重试'
starting_a_new_session: '发起新会话'
......
......@@ -118,7 +118,7 @@ home_module:
agent_welcome_message: 'Hi, 歡迎使用SuperLink'
agent_description: '在這裏,你可以體驗多個平臺的模型和專屬的智'
currently_in_the_latest_session: '當前已是最新會話'
switching_over: '切換中...'
switching_over: '切換中'
history_application_success: '歷史記錄應用成功'
history_application_failed_please_try_again: '歷史記錄應用失敗,請重試'
starting_a_new_session: '發起新會話'
......
import zhHK from './langs/zh-hk.yaml'
import zhCN from './langs/zh-cn.yaml'
import en from './langs/en.yaml'
const messages: Record<I18n.LangType, I18n.Schema> = {
'zh-HK': zhHK,
'zh-CN': zhCN,
en,
}
export default messages
import { ss } from '@/utils/storage'
import { defineStore } from 'pinia'
import { defaultLocale } from '@/locales/index'
interface SystemLanguageState {
currentLanguageInfo: {
key: string
label: string
}
languageOptions: {
key: string
label: string
}[]
}
const defaultLanguageOptions = [
{
label: '中文简体',
key: 'zh-CN',
},
{
label: '中文繁體',
key: 'zh-HK',
},
{
label: 'English',
key: 'en',
},
]
const localeKey = ss.get('i18nextLng') || defaultLocale
export const useSystemLanguageStore = defineStore('system-language-store', {
state: (): SystemLanguageState => ({
currentLanguageInfo: {
key: localeKey,
label: defaultLanguageOptions.find((optionItem) => optionItem.key === localeKey)!.label,
},
languageOptions: defaultLanguageOptions,
}),
actions: {
updateCurrentLanguageInfo(key: I18n.LangType) {
if (this.currentLanguageInfo.key === key) return ''
ss.set('i18nextLng', key)
this.currentLanguageInfo = this.languageOptions.find((optionItem) => optionItem.key === key) as {
key: string
label: string
}
},
},
})
// export const useSystemLanguageStore = defineStore('system-language-store', () => {
// const currentLanguageInfo = ref({
// key: localeKey,
// label: defaultLanguageOptions.find((optionItem) => optionItem.key === localeKey)!.label,
// })
// const languageOptions = readonly(defaultLanguageOptions)
// function updateCurrentLanguageInfo(key: I18n.LangType) {
// if (currentLanguageInfo.value.key === key) return ''
// ss.set('i18nextLng', key)
// const { locale } = useI18n()
// locale.value = key
// currentLanguageInfo.value = defaultLanguageOptions.find((optionItem) => optionItem.key === key) as {
// key: string
// label: string
// }
// }
// return { currentLanguageInfo, languageOptions, updateCurrentLanguageInfo }
// })
......@@ -150,7 +150,7 @@ function onHistoryRecordListUpdate() {
function onGetMessageRecordList(recordId: string) {
currentSessionId.value = recordId
const loadingCtl = window.$message.loading(t('home_module.switching_over'))
const loadingCtl = window.$message.loading(`${t('home_module.switching_over')}...`)
fetchMessageRecordList<
{
......
<script setup lang="ts">
import { ref, shallowReadonly, useTemplateRef, watchEffect } from 'vue'
import { computed, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vue'
import type { FormInst, FormRules, FormItemRule, CountdownInst } from 'naive-ui'
import { Mail, Lock, Iphone, Down, User } from '@icon-park/vue-next'
import isMobilePhone from 'validator/es/lib/isMobilePhone'
......@@ -11,6 +11,7 @@ import { useUserStore } from '@/store/modules/user'
import type { UserInfo } from '@/store/types/user'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import LanguageSetting from '@/components/language-setting/language-setting.vue'
enum StorageKeyEnum {
smsCountdownTime = 'SMS_COUNTDOWN_TIME',
......@@ -89,16 +90,18 @@ const emailLoginFormRules = shallowReadonly<FormRules>({
code: { required: true, message: t('login_module.please_enter_the_verification_code') },
})
const phoneNumberAreaOptions = shallowReadonly([
{
label: `+86 ${t('login_module.mainland_china')}`,
value: '+86',
},
{
label: `+852 ${t('login_module.hong_kong_china')}`,
value: '+852',
},
])
const phoneNumberAreaOptions = computed(() => {
return [
{
label: `+86 ${t('login_module.mainland_china')}`,
value: '+86',
},
{
label: `+852 ${t('login_module.hong_kong_china')}`,
value: '+852',
},
]
})
const currentPhoneNumberArea = ref<'+86' | '+852'>('+852')
const countdownActive = ref(true)
......@@ -304,6 +307,10 @@ function handleEmailCodeGain() {
class="bg-px-logo-png z-100 absolute left-[60px] top-[25px] h-[29px] w-[119px] bg-contain bg-center bg-no-repeat"
></div>
<div class="z-100 absolute right-[60px] top-[25px] w-[140px]">
<LanguageSetting arrow-direction="bottom" btn-bg-color="#f4f5f5" />
</div>
<div class="absolute right-[14%] top-1/2 h-[458px] w-[390px] -translate-y-1/2">
<div
class="h-full w-full rounded-[10px] bg-[#fff] px-[29px] shadow-2xl"
......@@ -412,9 +419,9 @@ function handleEmailCodeGain() {
>
<template #suffix>
<div class="flex items-center">
<div class="mx-[6px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="w-[90px] text-end">
<div class="text-end">
<n-button
v-show="!isShowCountdown"
class="!text-[11px]"
......@@ -486,9 +493,9 @@ function handleEmailCodeGain() {
>
<template #suffix>
<div class="flex items-center">
<div class="mx-[6px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="ml-[6px] mr-[10px] h-[18px] w-[1px] bg-[#868686]"></div>
<div class="w-[90px] text-end">
<div class="text-end">
<n-button
v-show="!isShowCountdown"
class="!text-[11px]"
......
declare namespace I18n {
type LangType = 'zh-HK' | 'zh-CN'
type LangType = 'zh-HK' | 'zh-CN' | 'en'
type Schema = {
common_module: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment