mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2026-10-06 13:42:51 +08:00
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 时,后续监听器仍收到本次更新的列表,而不是嵌套调用的新列表。
This commit is contained in:
@@ -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'] ?? {};
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SupportedLanguagesType = 'en-US' | 'zh-CN';
|
||||
export type SupportedLanguagesType = string;
|
||||
|
||||
export type ImportLocaleFn = () => Promise<{ default: Record<string, string> }>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user