feat(i18n): Implement internationalization support for your application
- Integrate the vue-i18n plugin and configure multilingual support - Add English and Chinese translation files
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-sql": "~2",
|
"@tauri-apps/plugin-sql": "~2",
|
||||||
"vue": "^3.5.21",
|
"vue": "^3.5.21",
|
||||||
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vuetify": "^3.10.1",
|
"vuetify": "^3.10.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
|||||||
15
src/App.vue
15
src/App.vue
@@ -14,7 +14,7 @@
|
|||||||
<v-list-item
|
<v-list-item
|
||||||
prepend-avatar="/src/assets/logo.svg"
|
prepend-avatar="/src/assets/logo.svg"
|
||||||
subtitle=""
|
subtitle=""
|
||||||
title="Spary"
|
:title="$t('app.title')"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
@@ -29,16 +29,20 @@
|
|||||||
></v-list-item>
|
></v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
prepend-icon="mdi-airport"
|
prepend-icon="mdi-airport"
|
||||||
title="Nodes"
|
:title="$t('app.nodes')"
|
||||||
value="nodes"
|
value="nodes"
|
||||||
@click="router.push('/nodes')"
|
@click="router.push('/nodes')"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
prepend-icon="mdi-cog"
|
prepend-icon="mdi-cog"
|
||||||
title="Settings"
|
:title="$t('app.settings')"
|
||||||
value="settings"
|
value="settings"
|
||||||
@click="router.push('/settings')"
|
@click="router.push('/settings')"
|
||||||
></v-list-item>
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<language-switcher />
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
@@ -54,8 +58,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import LanguageSwitcher from './components/LanguageSwitcher.vue'
|
||||||
import notificationProvider from './components/notify/notificationProvider.vue'
|
import notificationProvider from './components/notify/notificationProvider.vue'
|
||||||
|
|
||||||
const drawer = ref(true)
|
const drawer = ref(true)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
useI18n()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@@ -10,6 +10,7 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AddGroup: typeof import('./components/nodeEdit/addGroup.vue')['default']
|
AddGroup: typeof import('./components/nodeEdit/addGroup.vue')['default']
|
||||||
AddNode: typeof import('./components/nodeEdit/addNode.vue')['default']
|
AddNode: typeof import('./components/nodeEdit/addNode.vue')['default']
|
||||||
|
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
||||||
NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default']
|
NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default']
|
||||||
NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default']
|
NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default']
|
||||||
NotificationProvider: typeof import('./components/notify/notificationProvider.vue')['default']
|
NotificationProvider: typeof import('./components/notify/notificationProvider.vue')['default']
|
||||||
|
|||||||
48
src/components/LanguageSwitcher.vue
Normal file
48
src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
>
|
||||||
|
<v-icon>mdi-translate</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(item, i) in languages"
|
||||||
|
:key="i"
|
||||||
|
@click="changeLanguage(item.code)"
|
||||||
|
:active="currentLanguage === item.code"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ code: 'en', name: 'English' },
|
||||||
|
{ code: 'zh', name: '中文' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentLanguage = computed(() => locale.value);
|
||||||
|
|
||||||
|
const changeLanguage = (lang: string) => {
|
||||||
|
locale.value = lang;
|
||||||
|
// Store the selected language in localStorage so it persists across sessions
|
||||||
|
localStorage.setItem('language', lang);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the initial language from localStorage if available
|
||||||
|
const savedLanguage = localStorage.getItem('language');
|
||||||
|
if (savedLanguage && languages.some(lang => lang.code === savedLanguage)) {
|
||||||
|
locale.value = savedLanguage;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<v-container class="fill-height d-flex align-center justify-center">
|
<v-container class="fill-height d-flex align-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<v-switch
|
<v-switch
|
||||||
:label="String(functionStatus)"
|
:label="functionStatus === 'On' ? $t('spary.functionStatus.on') : $t('spary.functionStatus.off')"
|
||||||
:model-value="functionStatus === 'On'"
|
:model-value="functionStatus === 'On'"
|
||||||
@update:model-value="toggleFunctionStatus"
|
@update:model-value="toggleFunctionStatus"
|
||||||
></v-switch>
|
></v-switch>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed} from 'vue'
|
import {ref, computed} from 'vue'
|
||||||
import {invoke} from '@tauri-apps/api/core'
|
import {invoke} from '@tauri-apps/api/core'
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import {groupRepository} from "@/entities/group.ts";
|
import {groupRepository} from "@/entities/group.ts";
|
||||||
import {notify} from "@/components/notify/notifyStore.ts";
|
import {notify} from "@/components/notify/notifyStore.ts";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const groupName = ref('')
|
const groupName = ref('')
|
||||||
const groupNameRule = [
|
const groupNameRule = [
|
||||||
(value: string): boolean | string => {
|
(value: string): boolean | string => {
|
||||||
if (value?.length >= 1 && value?.length <= 15) return true
|
if (value?.length >= 1 && value?.length <= 15) return true
|
||||||
return 'Group name must be between 1 and 15 characters.'
|
return t('addGroup.groupNameRule')
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ const groupSubscribeUrlRule = [
|
|||||||
new URL(value)
|
new URL(value)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'URL is not valid.'
|
return t('addGroup.urlInvalid')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -41,7 +44,7 @@ async function addGroup() {
|
|||||||
const check_repeat_one = await groupRepository.findByName(groupName.value)
|
const check_repeat_one = await groupRepository.findByName(groupName.value)
|
||||||
console.log(check_repeat_one)
|
console.log(check_repeat_one)
|
||||||
if (check_repeat_one.length > 0) {
|
if (check_repeat_one.length > 0) {
|
||||||
notify("Group already exists.", {
|
notify(t('addGroup.groupExists'), {
|
||||||
color: "error"
|
color: "error"
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -55,7 +58,7 @@ async function addGroup() {
|
|||||||
arguments: groupArguments.value
|
arguments: groupArguments.value
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
notify("Group added.")
|
notify(t('addGroup.groupAdded'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,17 +80,17 @@ async function add_group(groupName: string, groupSubscribeUrl: string | null, gr
|
|||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="groupName"
|
v-model="groupName"
|
||||||
:rules="groupNameRule"
|
:rules="groupNameRule"
|
||||||
label="Group name"
|
:label="$t('addGroup.groupName')"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="groupSubscribeUrl"
|
v-model="groupSubscribeUrl"
|
||||||
:rules="groupSubscribeUrlRule"
|
:rules="groupSubscribeUrlRule"
|
||||||
label="Subscribe URL"
|
:label="$t('addGroup.subscribeUrl')"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-textarea
|
<v-textarea
|
||||||
v-model="groupArguments"
|
v-model="groupArguments"
|
||||||
label="Arguments"
|
:label="$t('addGroup.arguments')"
|
||||||
></v-textarea>
|
></v-textarea>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
@@ -96,7 +99,7 @@ async function add_group(groupName: string, groupSubscribeUrl: string | null, gr
|
|||||||
:disabled="isAddDisabled"
|
:disabled="isAddDisabled"
|
||||||
:loading="isAdding"
|
:loading="isAdding"
|
||||||
@click="addGroup">
|
@click="addGroup">
|
||||||
Add
|
{{ $t('addGroup.addGroupButton') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, onMounted, computed} from 'vue'
|
import {ref, onMounted, computed} from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import {Group, groupRepository} from "@/entities/group.ts";
|
import {Group, groupRepository} from "@/entities/group.ts";
|
||||||
import {nodeRepository} from "@/entities/node.ts";
|
import {nodeRepository} from "@/entities/node.ts";
|
||||||
import {notify} from "@/components/notify/notifyStore.ts";
|
import {notify} from "@/components/notify/notifyStore.ts";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{ groupId: string }>()
|
const props = defineProps<{ groupId: string }>()
|
||||||
|
|
||||||
const allGroups = ref<Group[]>([])
|
const allGroups = ref<Group[]>([])
|
||||||
@@ -36,7 +39,7 @@ async function addNode() {
|
|||||||
arguments: nodeArguments.value,
|
arguments: nodeArguments.value,
|
||||||
group_id: selectedGroupId.value
|
group_id: selectedGroupId.value
|
||||||
})
|
})
|
||||||
notify("Node added successfully")
|
notify(t('addNode.nodeAddedSuccess'))
|
||||||
isAdding.value = false
|
isAdding.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +50,7 @@ async function addNode() {
|
|||||||
<v-form fast-fail @submit.prevent>
|
<v-form fast-fail @submit.prevent>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="selectedGroupId"
|
v-model="selectedGroupId"
|
||||||
label="Group"
|
:label="$t('addNode.group')"
|
||||||
:items="allGroups"
|
:items="allGroups"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
@@ -56,12 +59,12 @@ async function addNode() {
|
|||||||
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="nodeAlias"
|
v-model="nodeAlias"
|
||||||
label="Node alias"
|
:label="$t('addNode.nodeAlias')"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
<v-textarea
|
<v-textarea
|
||||||
v-model="nodeArguments"
|
v-model="nodeArguments"
|
||||||
label="Arguments"
|
:label="$t('addNode.arguments')"
|
||||||
></v-textarea>
|
></v-textarea>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
@@ -70,7 +73,7 @@ async function addNode() {
|
|||||||
:disabled="isAddDisabled"
|
:disabled="isAddDisabled"
|
||||||
:loading="isAdding"
|
:loading="isAdding"
|
||||||
@click="addNode">
|
@click="addNode">
|
||||||
Add
|
{{ $t('addNode.addNodeButton') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref} from 'vue'
|
import {ref} from 'vue'
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import {groupRepository} from "@/entities/group.ts";
|
import {groupRepository} from "@/entities/group.ts";
|
||||||
import {nodeRepository} from "@/entities/node.ts";
|
import {nodeRepository} from "@/entities/node.ts";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
useI18n()
|
||||||
|
|
||||||
interface GroupItem {
|
interface GroupItem {
|
||||||
name: string
|
name: string
|
||||||
@@ -69,7 +71,7 @@ loadData()
|
|||||||
prepend-icon="mdi-plus"
|
prepend-icon="mdi-plus"
|
||||||
@click.stop="addItem(group)"
|
@click.stop="addItem(group)"
|
||||||
>
|
>
|
||||||
add
|
{{ $t('nodeList.add') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-expansion-panel-title>
|
</v-expansion-panel-title>
|
||||||
@@ -90,7 +92,7 @@ loadData()
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div>{{ item.url }}</div>
|
<div>{{ item.url }}</div>
|
||||||
<div>usage: {{ item.traffic }}</div>
|
<div>{{ $t('nodeList.usage', { traffic: item.traffic }) }}</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
useI18n()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -15,13 +17,13 @@ const router = useRouter()
|
|||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="success"></v-icon>
|
<v-icon color="success"></v-icon>
|
||||||
</template>
|
</template>
|
||||||
add group
|
{{ $t('nodesFloatButton.addGroup') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn key="2" prepend-icon="mdi-airplane-marker">
|
<v-btn key="2" prepend-icon="mdi-airplane-marker">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="success"></v-icon>
|
<v-icon color="success"></v-icon>
|
||||||
</template>
|
</template>
|
||||||
add node
|
{{ $t('nodesFloatButton.addNode') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-speed-dial>
|
</v-speed-dial>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
56
src/locales/en.json
Normal file
56
src/locales/en.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "Spary",
|
||||||
|
"nodes": "Nodes",
|
||||||
|
"settings": "Settings",
|
||||||
|
"add": "Add",
|
||||||
|
"group": "Group",
|
||||||
|
"node": "Node"
|
||||||
|
},
|
||||||
|
"spary": {
|
||||||
|
"functionStatus": {
|
||||||
|
"on": "On",
|
||||||
|
"off": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodeList": {
|
||||||
|
"add": "add",
|
||||||
|
"usage": "usage: {traffic}",
|
||||||
|
"noNodes": "No nodes available"
|
||||||
|
},
|
||||||
|
"nodesFloatButton": {
|
||||||
|
"addGroup": "add group",
|
||||||
|
"addNode": "add node"
|
||||||
|
},
|
||||||
|
"addGroup": {
|
||||||
|
"title": "Add Group",
|
||||||
|
"groupName": "Group name",
|
||||||
|
"groupNameRule": "Group name must be between 1 and 15 characters.",
|
||||||
|
"subscribeUrl": "Subscribe URL",
|
||||||
|
"urlInvalid": "URL is not valid.",
|
||||||
|
"arguments": "Arguments",
|
||||||
|
"addGroupButton": "Add",
|
||||||
|
"groupExists": "Group already exists.",
|
||||||
|
"groupAdded": "Group added."
|
||||||
|
},
|
||||||
|
"addNode": {
|
||||||
|
"title": "Add Node",
|
||||||
|
"group": "Group",
|
||||||
|
"nodeAlias": "Node alias",
|
||||||
|
"arguments": "Arguments",
|
||||||
|
"addNodeButton": "Add",
|
||||||
|
"nodeAddedSuccess": "Node added successfully"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"darkModeOn": "Dark Mode On",
|
||||||
|
"darkModeOff": "Dark Mode Off",
|
||||||
|
"language": "Language",
|
||||||
|
"english": "English",
|
||||||
|
"chinese": "Chinese"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/locales/zh.json
Normal file
56
src/locales/zh.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "Spary",
|
||||||
|
"nodes": "节点",
|
||||||
|
"settings": "设置",
|
||||||
|
"add": "添加",
|
||||||
|
"group": "组",
|
||||||
|
"node": "节点"
|
||||||
|
},
|
||||||
|
"spary": {
|
||||||
|
"functionStatus": {
|
||||||
|
"on": "开",
|
||||||
|
"off": "关"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodeList": {
|
||||||
|
"add": "添加",
|
||||||
|
"usage": "使用量: {traffic}",
|
||||||
|
"noNodes": "没有可用节点"
|
||||||
|
},
|
||||||
|
"nodesFloatButton": {
|
||||||
|
"addGroup": "添加组",
|
||||||
|
"addNode": "添加节点"
|
||||||
|
},
|
||||||
|
"addGroup": {
|
||||||
|
"title": "添加组",
|
||||||
|
"groupName": "组名称",
|
||||||
|
"groupNameRule": "组名称必须在1到15个字符之间。",
|
||||||
|
"subscribeUrl": "订阅链接",
|
||||||
|
"urlInvalid": "URL无效。",
|
||||||
|
"arguments": "参数",
|
||||||
|
"addGroupButton": "添加",
|
||||||
|
"groupExists": "组已存在。",
|
||||||
|
"groupAdded": "组已添加。"
|
||||||
|
},
|
||||||
|
"addNode": {
|
||||||
|
"title": "添加节点",
|
||||||
|
"group": "组",
|
||||||
|
"nodeAlias": "节点别名",
|
||||||
|
"arguments": "参数",
|
||||||
|
"addNodeButton": "添加",
|
||||||
|
"nodeAddedSuccess": "节点添加成功"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "取消",
|
||||||
|
"delete": "删除",
|
||||||
|
"edit": "编辑",
|
||||||
|
"confirm": "确认",
|
||||||
|
"darkModeOn": "深色模式开",
|
||||||
|
"darkModeOff": "深色模式关",
|
||||||
|
"language": "语言",
|
||||||
|
"english": "英语",
|
||||||
|
"chinese": "中文"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-card class="mx-auto" max-width="600">
|
||||||
|
<v-card-title>{{ $t('app.settings') }}</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-btn @click="toggleDarkMode" disabled>
|
||||||
|
{{ darkMode ? $t('common.darkModeOff') : $t('common.darkModeOn') }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<h3>{{ $t('common.language') }}</h3>
|
||||||
|
<v-btn-toggle v-model="selectedLanguage" mandatory>
|
||||||
|
<v-btn value="en">{{ $t('common.english') }}</v-btn>
|
||||||
|
<v-btn value="zh">{{ $t('common.chinese') }}</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
//
|
import { ref, watch, onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
const selectedLanguage = ref(locale.value);
|
||||||
|
const darkMode = ref(false);
|
||||||
|
|
||||||
|
// Update the translation when the language changes
|
||||||
|
watch(selectedLanguage, (newLang) => {
|
||||||
|
locale.value = newLang;
|
||||||
|
localStorage.setItem('language', newLang);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the selected language from localStorage
|
||||||
|
onMounted(() => {
|
||||||
|
const savedLanguage = localStorage.getItem('language');
|
||||||
|
if (savedLanguage) {
|
||||||
|
selectedLanguage.value = savedLanguage;
|
||||||
|
locale.value = savedLanguage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle dark mode
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
darkMode.value = !darkMode.value;
|
||||||
|
// Here you would typically update a theme preference
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
17
src/plugins/i18n.ts
Normal file
17
src/plugins/i18n.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import en from '@/locales/en.json'
|
||||||
|
import zh from '@/locales/zh.json'
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en: en,
|
||||||
|
zh: zh
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false, // Use composition API mode
|
||||||
|
locale: 'en', // Default locale
|
||||||
|
fallbackLocale: 'en', // Fallback locale
|
||||||
|
messages
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
// Plugins
|
// Plugins
|
||||||
import vuetify from './vuetify'
|
import vuetify from './vuetify'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
import i18n from './i18n'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
@@ -15,4 +16,5 @@ export function registerPlugins (app: App) {
|
|||||||
app
|
app
|
||||||
.use(vuetify)
|
.use(vuetify)
|
||||||
.use(router)
|
.use(router)
|
||||||
|
.use(i18n)
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/vue-i18n.d.ts
vendored
Normal file
7
src/vue-i18n.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import Vue from "vue";
|
||||||
|
declare module "@vue/runtime-core" {
|
||||||
|
export interface ComponentCustomProperties {
|
||||||
|
$t: (key: string, ...args: any[]) => string;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
yarn.lock
34
yarn.lock
@@ -304,6 +304,27 @@
|
|||||||
resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz"
|
resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz"
|
||||||
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
||||||
|
|
||||||
|
"@intlify/core-base@11.1.12":
|
||||||
|
version "11.1.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-11.1.12.tgz#e02529021a4e69f8a1adcca5ce61963c71cd72ba"
|
||||||
|
integrity sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==
|
||||||
|
dependencies:
|
||||||
|
"@intlify/message-compiler" "11.1.12"
|
||||||
|
"@intlify/shared" "11.1.12"
|
||||||
|
|
||||||
|
"@intlify/message-compiler@11.1.12":
|
||||||
|
version "11.1.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-11.1.12.tgz#27e69790b711a92cddb07175187dd09a7b270b55"
|
||||||
|
integrity sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==
|
||||||
|
dependencies:
|
||||||
|
"@intlify/shared" "11.1.12"
|
||||||
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
|
"@intlify/shared@11.1.12":
|
||||||
|
version "11.1.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-11.1.12.tgz#ab41083e017d622cf63c7dc88a0ee0bc27f4127a"
|
||||||
|
integrity sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==
|
||||||
|
|
||||||
"@jridgewell/gen-mapping@^0.3.5":
|
"@jridgewell/gen-mapping@^0.3.5":
|
||||||
version "0.3.13"
|
version "0.3.13"
|
||||||
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz"
|
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz"
|
||||||
@@ -877,7 +898,7 @@
|
|||||||
"@vue/compiler-dom" "3.5.22"
|
"@vue/compiler-dom" "3.5.22"
|
||||||
"@vue/shared" "3.5.22"
|
"@vue/shared" "3.5.22"
|
||||||
|
|
||||||
"@vue/devtools-api@^6.6.4":
|
"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4":
|
||||||
version "6.6.4"
|
version "6.6.4"
|
||||||
resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz"
|
resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz"
|
||||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||||
@@ -2429,7 +2450,7 @@ sisteransi@^1.0.5:
|
|||||||
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
|
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
|
||||||
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
||||||
|
|
||||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
|
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2, source-map-js@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||||
@@ -2688,6 +2709,15 @@ vue-eslint-parser@^10.2.0:
|
|||||||
esquery "^1.6.0"
|
esquery "^1.6.0"
|
||||||
semver "^7.6.3"
|
semver "^7.6.3"
|
||||||
|
|
||||||
|
vue-i18n@^11.1.12:
|
||||||
|
version "11.1.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.1.12.tgz#230b21d65dd89343ebd40d19c9e3cfb13db8c1fd"
|
||||||
|
integrity sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==
|
||||||
|
dependencies:
|
||||||
|
"@intlify/core-base" "11.1.12"
|
||||||
|
"@intlify/shared" "11.1.12"
|
||||||
|
"@vue/devtools-api" "^6.5.0"
|
||||||
|
|
||||||
vue-router@^4.5.1:
|
vue-router@^4.5.1:
|
||||||
version "4.5.1"
|
version "4.5.1"
|
||||||
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz"
|
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user