Browse Source

feat(locales): support runtime dynamic language list (#8279)

* feat(locales): support runtime dynamic language list

- 将 SupportedLanguagesType 从硬编码的 'en-US' | 'zh-CN' 联合类型放宽为 string,支持应用在运行时注册自定义语言

- @vben/constants 新增 setSupportLanguages / onSupportLanguagesChange / getSupportLanguages,以发布订阅方式动态管理语言列表(订阅时立即回调当前值)

- language-toggle 与 user-dropdown 组件改为订阅动态列表,不再直接引入静态 SUPPORT_LANGUAGES

- use-simple-locale 对未知语言回退到 en-US

- vxe-table 集成对未打包的自定义语言回退到 en-US 语言包

向后兼容:不调用 setSupportLanguages 时,行为与之前完全一致。

* fix(constants): 语言列表注册表返回独立快照

getSupportLanguages 与监听器回调改为返回克隆快照,存储时也做克隆,避免外部直接修改数组或元素对象污染注册表内部状态、绕过 setSupportLanguages 导致其他订阅者不同步。

* fix(constants): 偏好设置面板语言列表改为订阅动态注册表

- general.vue 不再直接引入静态 SUPPORT_LANGUAGES,改为通过 onSupportLanguagesChange 订阅,运行时替换语言列表后设置面板同步刷新

- setSupportLanguages 通知时迭代监听器快照,避免监听器在回调中取消订阅导致跳过后续监听器

* fix(constants): 固化本次更新快照后再通知监听器

监听器回调中嵌套调用 setSupportLanguages 时,后续监听器仍收到本次更新的列表,而不是嵌套调用的新列表。
pull/8273/head
徐蒙恩 4 days ago
committed by GitHub
parent
commit
53b1529298
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      packages/@core/composables/src/use-simple-locale/messages.ts
  2. 2
      packages/@core/preferences/src/types.ts
  3. 50
      packages/constants/src/core.ts
  4. 13
      packages/effects/layouts/src/widgets/language-toggle.vue
  5. 14
      packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue
  6. 14
      packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
  7. 11
      packages/effects/plugins/src/vxe-table/init.ts
  8. 2
      packages/locales/src/typing.ts

7
packages/@core/composables/src/use-simple-locale/messages.ts

@ -1,6 +1,6 @@
export type Locale = 'en-US' | 'zh-CN';
export type Locale = string;
export const messages: Record<Locale, Record<string, string>> = {
export const messages: Record<string, Record<string, string>> = {
'en-US': {
cancel: 'Cancel',
collapse: 'Collapse',
@ -25,4 +25,5 @@ export const messages: Record<Locale, Record<string, string>> = {
},
};
export const getMessages = (locale: Locale) => messages[locale];
export const getMessages = (locale: Locale) =>
messages[locale] ?? messages['en-US'] ?? {};

2
packages/@core/preferences/src/types.ts

@ -16,7 +16,7 @@ import type {
ThemeModeType,
} from '@vben-core/typings';
type SupportedLanguagesType = 'en-US' | 'zh-CN';
type SupportedLanguagesType = string;
type CustomPreferencesValue = boolean | number | string;
interface CustomPreferencesOption<TValue extends string = string> {

50
packages/constants/src/core.ts

@ -5,11 +5,11 @@ export const LOGIN_PATH = '/auth/login';
export interface LanguageOption {
label: string;
value: 'en-US' | 'zh-CN';
value: string;
}
/**
* Supported languages
* setSupportLanguages
*/
export const SUPPORT_LANGUAGES: LanguageOption[] = [
{
@ -21,3 +21,49 @@ export const SUPPORT_LANGUAGES: LanguageOption[] = [
value: 'en-US',
},
];
// --- 动态语言列表管理 ---
type LanguageListListener = (languages: LanguageOption[]) => void;
// 深拷贝语言列表,外部拿到的永远是独立快照,
// 避免绕过 setSupportLanguages 直接修改注册表内部状态
function cloneLanguages(languages: LanguageOption[]): LanguageOption[] {
return languages.map((language) => ({ ...language }));
}
let currentLanguages: LanguageOption[] = cloneLanguages(SUPPORT_LANGUAGES);
const listeners: LanguageListListener[] = [];
/**
*
*/
export function getSupportLanguages(): LanguageOption[] {
return cloneLanguages(currentLanguages);
}
/**
*
*/
export function setSupportLanguages(languages: LanguageOption[]) {
// 先固化本次快照,即使监听器回调中嵌套调用 setSupportLanguages,
// 后续监听器收到的仍是本次更新的列表
const nextLanguages = cloneLanguages(languages);
currentLanguages = nextLanguages;
// 迭代监听器快照,避免监听器在回调中取消订阅导致跳过后续监听器
for (const listener of listeners.slice()) {
listener(cloneLanguages(nextLanguages));
}
}
/**
*
*/
export function onSupportLanguagesChange(listener: LanguageListListener) {
listeners.push(listener);
// 订阅时立即回调当前值,便于消费方初始化
listener(cloneLanguages(currentLanguages));
return () => {
const index = listeners.indexOf(listener);
if (index !== -1) listeners.splice(index, 1);
};
}

13
packages/effects/layouts/src/widgets/language-toggle.vue

@ -1,7 +1,10 @@
<script setup lang="ts">
import type { LanguageOption } from '@vben/constants';
import type { SupportedLanguagesType } from '@vben/locales';
import { SUPPORT_LANGUAGES } from '@vben/constants';
import { onUnmounted, ref } from 'vue';
import { onSupportLanguagesChange } from '@vben/constants';
import { Languages } from '@vben/icons';
import { loadLocaleMessages } from '@vben/locales';
import { preferences, updatePreferences } from '@vben/preferences';
@ -12,6 +15,12 @@ defineOptions({
name: 'LanguageToggle',
});
const languageList = ref<LanguageOption[]>([]);
const unsubscribe = onSupportLanguagesChange((langs) => {
languageList.value = [...langs];
});
onUnmounted(unsubscribe);
async function handleUpdate(value: string | undefined) {
if (!value) return;
const locale = value as SupportedLanguagesType;
@ -27,7 +36,7 @@ async function handleUpdate(value: string | undefined) {
<template>
<div>
<VbenDropdownRadioMenu
:menus="SUPPORT_LANGUAGES"
:menus="languageList"
:model-value="preferences.app.locale"
@update:model-value="handleUpdate"
>

14
packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue

@ -1,7 +1,9 @@
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue';
import type { LanguageOption } from '@vben/constants';
import { SUPPORT_LANGUAGES } from '@vben/constants';
import { onMounted, onUnmounted, ref, unref } from 'vue';
import { onSupportLanguagesChange } from '@vben/constants';
import { $t } from '@vben/locales';
import { useTimezoneStore } from '@vben/stores';
@ -24,6 +26,12 @@ const appEnableCopyPreferences = defineModel<boolean>(
);
const timezoneStore = useTimezoneStore();
const languageList = ref<LanguageOption[]>([]);
const unsubscribe = onSupportLanguagesChange((langs) => {
languageList.value = [...langs];
});
onUnmounted(unsubscribe);
const timezoneOptionsRef = ref<
{
label: string;
@ -42,7 +50,7 @@ onMounted(async () => {
</script>
<template>
<SelectItem v-model="appLocale" :items="SUPPORT_LANGUAGES">
<SelectItem v-model="appLocale" :items="languageList">
{{ $t('preferences.language') }}
</SelectItem>
<SelectItem v-model="appTimezone" :items="timezoneOptionsRef">

14
packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue

@ -1,11 +1,12 @@
<script setup lang="ts">
import type { Component } from 'vue';
import type { LanguageOption } from '@vben/constants';
import type { AnyFunction } from '@vben/types';
import { computed, ref, useTemplateRef, watch } from 'vue';
import { computed, onUnmounted, ref, useTemplateRef, watch } from 'vue';
import { SUPPORT_LANGUAGES } from '@vben/constants';
import { onSupportLanguagesChange } from '@vben/constants';
import { useHoverToggle, useRefresh } from '@vben/hooks';
import {
createIconifyIcon,
@ -278,11 +279,16 @@ function handleNotificationSelect(event?: Event) {
// - Radix dropdown/ locale
const showLanguageList = ref(false);
const languageList = ref<LanguageOption[]>([]);
const unsubLang = onSupportLanguagesChange((langs) => {
languageList.value = [...langs];
});
onUnmounted(unsubLang);
function handleLanguageToggleSelect(event?: Event) {
event?.preventDefault();
showLanguageList.value = !showLanguageList.value;
}
async function handleLocaleChange(event: Event, value: 'en-US' | 'zh-CN') {
async function handleLocaleChange(event: Event, value: string) {
//
event.preventDefault();
updatePreferences({ app: { locale: value } });
@ -480,7 +486,7 @@ if (preferences.shortcutKeys.enable) {
</DropdownMenuItem>
<template v-if="showLanguageList">
<DropdownMenuItem
v-for="lang in SUPPORT_LANGUAGES"
v-for="lang in languageList"
:key="lang.value"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 pl-8 leading-8"
@select="(e: Event) => handleLocaleChange(e, lang.value)"

11
packages/effects/plugins/src/vxe-table/init.ts

@ -119,7 +119,7 @@ export function setupVbenVxeTable(setupOptions: SetupVxeTable) {
}
const { isDark, locale } = usePreferences();
const localMap = {
const localMap: Record<string, any> = {
'zh-CN': normalizeVxeLocale(zhCN),
'en-US': normalizeVxeLocale(enUS),
};
@ -128,8 +128,13 @@ export function setupVbenVxeTable(setupOptions: SetupVxeTable) {
[() => isDark.value, () => locale.value],
([isDarkValue, localeValue]) => {
VxeUI.setTheme(isDarkValue ? 'dark' : 'light');
VxeUI.setI18n(localeValue, localMap[localeValue]);
VxeUI.setLanguage(localeValue);
// 应用语言是运行时字符串(见 SupportedLanguagesType);vxe-table 只为内置
// 语言提供类型定义,因此这里需要断言,未打包的自定义语言回退到 en-US
VxeUI.setI18n(
localeValue as any,
localMap[localeValue] ?? localMap['en-US'],
);
VxeUI.setLanguage(localeValue as any);
},
{
immediate: true,

2
packages/locales/src/typing.ts

@ -1,4 +1,4 @@
export type SupportedLanguagesType = 'en-US' | 'zh-CN';
export type SupportedLanguagesType = string;
export type ImportLocaleFn = () => Promise<{ default: Record<string, string> }>;

Loading…
Cancel
Save