Skip to content

Commit 4ccb89b

Browse files
committed
feat(i18n): Implement dynamic language loading
1 parent 7d5c69d commit 4ccb89b

File tree

16 files changed

+140
-78
lines changed

16 files changed

+140
-78
lines changed

frontend/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ VITE_APP_TITLE = GUI.for.Clash
22
VITE_APP_VERSION = v1.9.10
33
VITE_APP_PROJECT_URL = https://github.com/GUI-for-Cores
44
VITE_APP_VERSION_API = https://api.github.com/repos/GUI-for-Cores/GUI.for.Clash/releases/latest
5+
VITE_APP_LOCALES_URL = https://github.com/GUI-for-Cores/GUI-for-Cores.github.io/tree/main/app-resources/locales/gfc
56
VITE_APP_TG_GROUP = https://t.me/GUI_for_Cores
67
VITE_APP_TG_CHANNEL = https://t.me/GUI_for_Cores_Channel

frontend/env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ interface ImportMetaEnv {
44
readonly VITE_APP_TITLE: string
55
readonly VITE_APP_VERSION: string
66
readonly VITE_APP_PROJECT_URL: string
7+
readonly VITE_APP_LOCALES_URL: string
78
readonly VITE_APP_TG_GROUP: string
89
readonly VITE_APP_TG_CHANNEL: string
910
readonly VITE_APP_VERSION_API: string

frontend/src/components/Dropdown/index.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ watch(show, async (isVisible) => {
9797
9898
const open = () => (show.value = true)
9999
const close = () => (show.value = false)
100+
const toggle = () => (show.value = !show.value)
100101
const hasTrigger = (t: TriggerType) => props.trigger.includes(t)
101102
102103
const onMouseEnter = () => {
@@ -146,7 +147,7 @@ onUnmounted(() => {
146147
@click="onClick"
147148
class="gui-dropdown relative flex flex-col items-center"
148149
>
149-
<slot :="{ open, close }"></slot>
150+
<slot v-bind="{ open, close, toggle }"></slot>
150151
<Transition name="overlay">
151152
<div
152153
v-show="show"
@@ -155,7 +156,7 @@ onUnmounted(() => {
155156
class="gui-dropdown-overlay fixed z-99 rounded-8 backdrop-blur-sm shadow overflow-y-auto"
156157
@click.stop
157158
>
158-
<slot name="overlay" :="{ open, close }"></slot>
159+
<slot name="overlay" v-bind="{ open, close, toggle }"></slot>
159160
</div>
160161
</Transition>
161162
</div>

frontend/src/components/Radio/index.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ const handleSelect = (val: string | number | boolean) => {
2424
</script>
2525

2626
<template>
27-
<div :class="[size]" class="gui-radio inline-flex rounded-8 text-12 overflow-hidden">
27+
<div :class="[size]" class="gui-radio inline-flex rounded-full text-12 overflow-hidden">
2828
<div
2929
v-for="o in options"
3030
:key="o.value.toString()"
31+
v-tips.slow="o.label"
3132
@click="handleSelect(o.value)"
3233
:class="{ active: o.value === model }"
33-
class="gui-radio-button cursor-pointer px-12 py-6 duration-200"
34+
class="gui-radio-button cursor-pointer px-12 py-6 duration-200 line-clamp-1 break-all"
3435
>
3536
{{ t(o.label) }}
3637
</div>

frontend/src/components/Select/index.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ const handleClear = () => {
4040

4141
<template>
4242
<Dropdown :trigger="['click']">
43-
<template #default="{ open }">
43+
<template #default="{ toggle }">
4444
<div
4545
:class="{ border, [size]: true, 'auto-size': autoSize }"
4646
class="gui-select cursor-pointer min-h-30 inline-flex items-center min-w-128 rounded-4 px-8"
4747
>
48-
{{ t(displayLabel) }}
48+
<span class="line-clamp-1 break-all">
49+
{{ t(displayLabel) }}
50+
</span>
4951
<Button
5052
:icon="clearable && model ? 'close' : 'arrowDown'"
51-
@click.stop="() => (clearable && model ? handleClear() : open())"
53+
@click.stop="() => (clearable && model ? handleClear() : toggle())"
5254
type="text"
5355
size="small"
5456
class="ml-auto"

frontend/src/constant/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
View,
88
} from '@/enums/app'
99

10+
export const LocalesFilePath = 'data/locales'
11+
1012
export const UserFilePath = 'data/user.yaml'
1113

1214
export const ProfilesFilePath = 'data/profiles.yaml'

frontend/src/lang/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { createI18n } from 'vue-i18n'
22

3+
import { ReadFile } from '@/bridge'
4+
import { LocalesFilePath } from '@/constant/app'
5+
36
import en from './locale/en'
47
import zh from './locale/zh'
58

6-
const messages = {
9+
const messages: { [key: string]: Recordable } = {
710
zh,
811
en,
912
}
@@ -16,4 +19,11 @@ const i18n = createI18n({
1619
messages,
1720
})
1821

22+
export const loadLocaleMessages = async (locale: string) => {
23+
if (!i18n.global.availableLocales.includes(locale)) {
24+
const messages = await ReadFile(`${LocalesFilePath}/${locale}.json`)
25+
i18n.global.setLocaleMessage(locale, JSON.parse(messages))
26+
}
27+
}
28+
1929
export default i18n

frontend/src/lang/locale/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ export default {
569569
},
570570
lang: {
571571
name: 'Language',
572+
load: 'Load language files',
572573
zh: '简体中文',
573574
en: 'English',
574575
},

frontend/src/lang/locale/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ export default {
568568
},
569569
lang: {
570570
name: '语言',
571+
load: '加载语言文件',
571572
zh: '简体中文',
572573
en: 'English',
573574
},

frontend/src/stores/appSettings.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ref, watch } from 'vue'
33
import { parse, stringify } from 'yaml'
44

55
import {
6+
ReadDir,
67
ReadFile,
78
WriteFile,
89
WindowSetSystemDefaultTheme,
@@ -17,6 +18,7 @@ import {
1718
DefaultFontFamily,
1819
DefaultTestURL,
1920
UserFilePath,
21+
LocalesFilePath,
2022
} from '@/constant/app'
2123
import { CorePidFilePath, DefaultConnections, DefaultCoreConfig } from '@/constant/kernel'
2224
import {
@@ -29,15 +31,12 @@ import {
2931
ControllerCloseMode,
3032
Branch,
3133
} from '@/enums/app'
32-
import i18n from '@/lang'
33-
import { debounce, updateTrayMenus, APP_TITLE, ignoredError, APP_VERSION } from '@/utils'
34+
import i18n, { loadLocaleMessages } from '@/lang'
35+
import { debounce, updateTrayMenus, APP_TITLE, ignoredError, APP_VERSION, sleep } from '@/utils'
3436

3537
import type { AppSettings } from '@/types/app'
3638

3739
export const useAppSettingsStore = defineStore('app-settings', () => {
38-
let firstOpen = true
39-
let latestUserConfig = ''
40-
4140
const themeMode = ref<Theme.Dark | Theme.Light>(Theme.Light)
4241

4342
const app = ref<AppSettings>({
@@ -91,9 +90,46 @@ export const useAppSettingsStore = defineStore('app-settings', () => {
9190
WriteFile(UserFilePath, config)
9291
}, 500)
9392

93+
const localesLoading = ref(false)
94+
const locales = ref<{ label: string; value: string }[]>([])
95+
const loadLocales = async (delay = false) => {
96+
localesLoading.value = true
97+
locales.value = [
98+
{
99+
label: 'settings.lang.zh',
100+
value: Lang.ZH,
101+
},
102+
{
103+
label: 'settings.lang.en',
104+
value: Lang.EN,
105+
},
106+
]
107+
const dirs = await ignoredError(ReadDir, LocalesFilePath)
108+
if (dirs) {
109+
const files = dirs.flatMap((file) => {
110+
if (file.isDir) return []
111+
const [name, ext] = file.name.split('.')
112+
return name && ext === 'json' ? { label: name, value: name } : []
113+
})
114+
locales.value.push(...files)
115+
}
116+
delay && (await sleep(200))
117+
localesLoading.value = false
118+
}
119+
120+
let latestUserSettings: string
121+
94122
const setupAppSettings = async () => {
95123
const data = await ignoredError(ReadFile, UserFilePath)
96-
data && (app.value = Object.assign(app.value, parse(data)))
124+
if (data) {
125+
const settings = parse(data)
126+
latestUserSettings = stringify(settings)
127+
app.value = Object.assign(app.value, settings)
128+
} else {
129+
latestUserSettings = ''
130+
}
131+
132+
await loadLocales()
97133

98134
if (!app.value.kernel.main) {
99135
app.value.kernel.main = DefaultCoreConfig()
@@ -132,10 +168,6 @@ export const useAppSettingsStore = defineStore('app-settings', () => {
132168
// @ts-expect-error(Deprecated)
133169
delete app.value.kernel.pid
134170
}
135-
136-
firstOpen = !!data
137-
138-
updateAppSettings(app.value)
139171
}
140172

141173
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')
@@ -157,6 +189,7 @@ export const useAppSettingsStore = defineStore('app-settings', () => {
157189
}
158190

159191
const updateAppSettings = (settings: AppSettings) => {
192+
loadLocaleMessages(settings.lang)
160193
i18n.global.locale.value = settings.lang
161194
themeMode.value =
162195
settings.theme === Theme.Auto
@@ -177,20 +210,16 @@ export const useAppSettingsStore = defineStore('app-settings', () => {
177210
(settings) => {
178211
updateAppSettings(settings)
179212

180-
if (!firstOpen) {
181-
const lastModifiedConfig = stringify(settings)
182-
if (latestUserConfig !== lastModifiedConfig) {
183-
saveAppSettings(lastModifiedConfig).then(() => {
184-
latestUserConfig = lastModifiedConfig
185-
})
186-
} else {
187-
saveAppSettings.cancel()
188-
}
213+
const lastModifiedSettings = stringify(settings)
214+
if (latestUserSettings !== undefined && latestUserSettings !== lastModifiedSettings) {
215+
saveAppSettings(lastModifiedSettings).then(() => {
216+
latestUserSettings = lastModifiedSettings
217+
})
218+
} else {
219+
saveAppSettings.cancel()
189220
}
190-
191-
firstOpen = false
192221
},
193-
{ deep: true },
222+
{ deep: true, immediate: true },
194223
)
195224

196225
window.addEventListener(
@@ -208,11 +237,16 @@ export const useAppSettingsStore = defineStore('app-settings', () => {
208237
)
209238

210239
watch(
211-
[themeMode, () => app.value.color, () => app.value.lang, () => app.value.addPluginToMenu],
240+
[
241+
themeMode,
242+
locales,
243+
() => app.value.color,
244+
() => app.value.lang,
245+
() => app.value.addPluginToMenu,
246+
],
212247
updateTrayMenus,
213248
)
214-
215249
watch(themeMode, setAppTheme, { immediate: true })
216250

217-
return { setupAppSettings, app, themeMode }
251+
return { setupAppSettings, app, themeMode, locales, localesLoading, loadLocales }
218252
})

0 commit comments

Comments
 (0)