mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 23:57:19 +08:00
feat: add default language configuration
This commit is contained in:
@@ -13,6 +13,7 @@ from .types import ConfigItem
|
|||||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||||
|
|
||||||
PUBLIC_CONFIG_KEYS = [
|
PUBLIC_CONFIG_KEYS = [
|
||||||
|
"APP_DEFAULT_LANGUAGE",
|
||||||
"THEME_MODE",
|
"THEME_MODE",
|
||||||
"THEME_PRIMARY_COLOR",
|
"THEME_PRIMARY_COLOR",
|
||||||
"THEME_BORDER_RADIUS",
|
"THEME_BORDER_RADIUS",
|
||||||
@@ -56,6 +57,7 @@ async def get_all_config(
|
|||||||
configs = await ConfigService.get_all()
|
configs = await ConfigService.get_all()
|
||||||
return success(configs)
|
return success(configs)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/public")
|
@router.get("/public")
|
||||||
@audit(action=AuditAction.READ, description="获取公开配置")
|
@audit(action=AuditAction.READ, description="获取公开配置")
|
||||||
async def get_public_config(
|
async def get_public_config(
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class ConfigService:
|
|||||||
logo=logo,
|
logo=logo,
|
||||||
favicon=favicon,
|
favicon=favicon,
|
||||||
is_initialized=user_count > 0,
|
is_initialized=user_count > 0,
|
||||||
|
default_language=await cls.get("APP_DEFAULT_LANGUAGE", "zh"),
|
||||||
app_domain=await cls.get("APP_DOMAIN"),
|
app_domain=await cls.get("APP_DOMAIN"),
|
||||||
file_domain=await cls.get("FILE_DOMAIN"),
|
file_domain=await cls.get("FILE_DOMAIN"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class SystemStatus(BaseModel):
|
|||||||
logo: str
|
logo: str
|
||||||
favicon: str
|
favicon: str
|
||||||
is_initialized: bool
|
is_initialized: bool
|
||||||
|
default_language: str = "zh"
|
||||||
app_domain: Optional[str] = None
|
app_domain: Optional[str] = None
|
||||||
file_domain: Optional[str] = None
|
file_domain: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
+35
-34
@@ -10,38 +10,7 @@ import { Routes, Route, Navigate } from 'react-router';
|
|||||||
import SetupPage from './pages/SetupPage.tsx';
|
import SetupPage from './pages/SetupPage.tsx';
|
||||||
import { I18nProvider } from './i18n';
|
import { I18nProvider } from './i18n';
|
||||||
|
|
||||||
function AppInner() {
|
function AppInner({ status }: { status: SystemStatus }) {
|
||||||
const [status, setStatus] = useState<SystemStatus | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
async function checkInitialization() {
|
|
||||||
try {
|
|
||||||
const status = await getStatus();
|
|
||||||
setStatus(status);
|
|
||||||
document.title = status.title;
|
|
||||||
let favicon = document.querySelector("link[rel*='icon']") as HTMLLinkElement | null;
|
|
||||||
if (!favicon) {
|
|
||||||
favicon = document.createElement('link');
|
|
||||||
favicon.rel = 'icon';
|
|
||||||
document.head.appendChild(favicon);
|
|
||||||
}
|
|
||||||
if (favicon) {
|
|
||||||
favicon.href = status.favicon || status.logo;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to check initialization status:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkInitialization();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (status === null) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
|
||||||
<Spin size="large" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SystemContext.Provider value={status}>
|
<SystemContext.Provider value={status}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
@@ -61,9 +30,41 @@ function AppInner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [status, setStatus] = useState<SystemStatus | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function checkInitialization() {
|
||||||
|
try {
|
||||||
|
const nextStatus = await getStatus();
|
||||||
|
setStatus(nextStatus);
|
||||||
|
document.title = nextStatus.title;
|
||||||
|
let favicon = document.querySelector("link[rel*='icon']") as HTMLLinkElement | null;
|
||||||
|
if (!favicon) {
|
||||||
|
favicon = document.createElement('link');
|
||||||
|
favicon.rel = 'icon';
|
||||||
|
document.head.appendChild(favicon);
|
||||||
|
}
|
||||||
|
if (favicon) {
|
||||||
|
favicon.href = nextStatus.favicon || nextStatus.logo;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check initialization status:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkInitialization();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (status === null) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<I18nProvider defaultLanguage={status.default_language}>
|
||||||
<AppInner />
|
<AppInner status={status} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import request from './client';
|
import request from './client';
|
||||||
|
import type { Lang } from '../i18n/lang';
|
||||||
|
|
||||||
export async function getConfig(key: string) {
|
export async function getConfig(key: string) {
|
||||||
return request<{ key: string; value: string }>('/config/?key=' + encodeURIComponent(key));
|
return request<{ key: string; value: string }>('/config/?key=' + encodeURIComponent(key));
|
||||||
@@ -25,6 +26,7 @@ export interface SystemStatus {
|
|||||||
logo: string;
|
logo: string;
|
||||||
favicon: string;
|
favicon: string;
|
||||||
is_initialized: boolean;
|
is_initialized: boolean;
|
||||||
|
default_language?: Lang;
|
||||||
app_domain?: string;
|
app_domain?: string;
|
||||||
file_domain?: string;
|
file_domain?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import type { AppComponentProps, AppOpenComponentProps } from '../types';
|
import type { AppComponentProps, AppOpenComponentProps } from '../types';
|
||||||
import type { PluginItem } from '../../api/plugins';
|
import type { PluginItem } from '../../api/plugins';
|
||||||
|
import { useI18n } from '../../i18n';
|
||||||
|
|
||||||
export interface PluginAppHostProps extends AppComponentProps {
|
export interface PluginAppHostProps extends AppComponentProps {
|
||||||
plugin: PluginItem;
|
plugin: PluginItem;
|
||||||
@@ -34,6 +35,7 @@ export const PluginAppHost: React.FC<PluginAppHostProps> = ({
|
|||||||
entry,
|
entry,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { lang } = useI18n();
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const onCloseRef = useRef(onRequestClose);
|
const onCloseRef = useRef(onRequestClose);
|
||||||
onCloseRef.current = onRequestClose;
|
onCloseRef.current = onRequestClose;
|
||||||
@@ -45,10 +47,11 @@ export const PluginAppHost: React.FC<PluginAppHostProps> = ({
|
|||||||
pluginVersion: plugin.version || '',
|
pluginVersion: plugin.version || '',
|
||||||
pluginStyles: JSON.stringify(getPluginStylePaths(plugin)),
|
pluginStyles: JSON.stringify(getPluginStylePaths(plugin)),
|
||||||
mode: 'file',
|
mode: 'file',
|
||||||
|
lang,
|
||||||
filePath,
|
filePath,
|
||||||
entry: JSON.stringify(entry),
|
entry: JSON.stringify(entry),
|
||||||
}),
|
}),
|
||||||
[plugin, filePath, entry]
|
[plugin, filePath, entry, lang]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -86,6 +89,7 @@ export interface PluginAppOpenHostProps extends AppOpenComponentProps {
|
|||||||
* 注意:同源且不加 sandbox 时,不是安全沙箱(插件仍可通过 window.parent 访问宿主)。
|
* 注意:同源且不加 sandbox 时,不是安全沙箱(插件仍可通过 window.parent 访问宿主)。
|
||||||
*/
|
*/
|
||||||
export const PluginAppOpenHost: React.FC<PluginAppOpenHostProps> = ({ plugin, onRequestClose }) => {
|
export const PluginAppOpenHost: React.FC<PluginAppOpenHostProps> = ({ plugin, onRequestClose }) => {
|
||||||
|
const { lang } = useI18n();
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const onCloseRef = useRef(onRequestClose);
|
const onCloseRef = useRef(onRequestClose);
|
||||||
onCloseRef.current = onRequestClose;
|
onCloseRef.current = onRequestClose;
|
||||||
@@ -97,8 +101,9 @@ export const PluginAppOpenHost: React.FC<PluginAppOpenHostProps> = ({ plugin, on
|
|||||||
pluginVersion: plugin.version || '',
|
pluginVersion: plugin.version || '',
|
||||||
pluginStyles: JSON.stringify(getPluginStylePaths(plugin)),
|
pluginStyles: JSON.stringify(getPluginStylePaths(plugin)),
|
||||||
mode: 'app',
|
mode: 'app',
|
||||||
|
lang,
|
||||||
}),
|
}),
|
||||||
[plugin]
|
[plugin, lang]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
+25
-8
@@ -2,8 +2,8 @@ import { createContext, useCallback, useContext, useMemo, useState, useEffect }
|
|||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import en from './locales/en.json';
|
import en from './locales/en.json';
|
||||||
import zhOverrides from './locales/zh.json';
|
import zhOverrides from './locales/zh.json';
|
||||||
|
import { normalizeLang, persistLang, readStoredLang, type Lang } from './lang';
|
||||||
|
|
||||||
type Lang = 'zh' | 'en';
|
|
||||||
type Dict = Record<string, string>;
|
type Dict = Record<string, string>;
|
||||||
|
|
||||||
const dicts: Record<Lang, Dict> = {
|
const dicts: Record<Lang, Dict> = {
|
||||||
@@ -11,9 +11,13 @@ const dicts: Record<Lang, Dict> = {
|
|||||||
zh: { ...en, ...zhOverrides },
|
zh: { ...en, ...zhOverrides },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface SetLangOptions {
|
||||||
|
persist?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface I18nContextValue {
|
export interface I18nContextValue {
|
||||||
lang: Lang;
|
lang: Lang;
|
||||||
setLang: (lang: Lang) => void;
|
setLang: (lang: Lang, options?: SetLangOptions) => void;
|
||||||
t: (key: string, params?: Record<string, string | number>) => string;
|
t: (key: string, params?: Record<string, string | number>) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,13 +28,26 @@ function interpolate(template: string, params?: Record<string, string | number>)
|
|||||||
return template.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`));
|
return template.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function I18nProvider({ children }: PropsWithChildren) {
|
interface I18nProviderProps {
|
||||||
const [lang, setLangState] = useState<Lang>(() => (localStorage.getItem('lang') as Lang) || 'zh');
|
defaultLanguage?: Lang;
|
||||||
|
}
|
||||||
|
|
||||||
const setLang = useCallback((l: Lang) => {
|
export function I18nProvider({ children, defaultLanguage }: PropsWithChildren<I18nProviderProps>) {
|
||||||
setLangState(l);
|
const fallbackLang = normalizeLang(defaultLanguage, 'zh');
|
||||||
localStorage.setItem('lang', l);
|
const [lang, setLangState] = useState<Lang>(() => readStoredLang() ?? fallbackLang);
|
||||||
}, []);
|
|
||||||
|
const setLang = useCallback((nextLang: Lang, options?: SetLangOptions) => {
|
||||||
|
const normalized = normalizeLang(nextLang, fallbackLang);
|
||||||
|
setLangState(normalized);
|
||||||
|
if (options?.persist === false) return;
|
||||||
|
persistLang(normalized);
|
||||||
|
}, [fallbackLang]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!readStoredLang()) {
|
||||||
|
setLangState(fallbackLang);
|
||||||
|
}
|
||||||
|
}, [fallbackLang]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.lang = lang;
|
document.documentElement.lang = lang;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
export type Lang = 'zh' | 'en';
|
||||||
|
|
||||||
|
const LANG_STORAGE_KEY = 'lang';
|
||||||
|
|
||||||
|
export function parseLang(raw: unknown): Lang | null {
|
||||||
|
if (typeof raw !== 'string') return null;
|
||||||
|
const value = raw.trim().toLowerCase();
|
||||||
|
if (!value) return null;
|
||||||
|
if (value === 'en' || value.startsWith('en-')) return 'en';
|
||||||
|
if (value === 'zh' || value.startsWith('zh-')) return 'zh';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeLang(raw: unknown, fallback: Lang = 'zh'): Lang {
|
||||||
|
return parseLang(raw) ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readStoredLang(): Lang | null {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
try {
|
||||||
|
return parseLang(window.localStorage.getItem(LANG_STORAGE_KEY));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistLang(lang: Lang): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(LANG_STORAGE_KEY, lang);
|
||||||
|
} catch {
|
||||||
|
void 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveLang(fallback: Lang = 'zh'): Lang {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const documentLang = parseLang(document.documentElement.lang);
|
||||||
|
if (documentLang) return documentLang;
|
||||||
|
}
|
||||||
|
return readStoredLang() ?? fallback;
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@
|
|||||||
"Language": "Language",
|
"Language": "Language",
|
||||||
"Chinese": "中文",
|
"Chinese": "中文",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
|
"Default Language": "Default Language",
|
||||||
|
"Used when the user has not selected a language": "Used when the user has not selected a language",
|
||||||
"Full Name": "Full Name",
|
"Full Name": "Full Name",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"Change Password": "Change Password",
|
"Change Password": "Change Password",
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
"Profile": "个人资料",
|
"Profile": "个人资料",
|
||||||
"Account Settings": "账户设置",
|
"Account Settings": "账户设置",
|
||||||
"Language": "语言",
|
"Language": "语言",
|
||||||
|
"Chinese": "中文",
|
||||||
|
"English": "English",
|
||||||
|
"Default Language": "默认语言",
|
||||||
|
"Used when the user has not selected a language": "用户未手动选择语言时使用",
|
||||||
"Full Name": "昵称",
|
"Full Name": "昵称",
|
||||||
"Email": "邮箱",
|
"Email": "邮箱",
|
||||||
"Change Password": "修改密码",
|
"Change Password": "修改密码",
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
|
|||||||
});
|
});
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const handleSave = async (values: Record<string, unknown>) => {
|
const handleSave = async (values: Record<string, unknown>): Promise<boolean> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
for (const [key, value] of Object.entries(values)) {
|
for (const [key, value] of Object.entries(values)) {
|
||||||
@@ -81,10 +81,13 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
|
|||||||
if (Object.keys(values).some(k => Object.values(THEME_KEYS).includes(k))) {
|
if (Object.keys(values).some(k => Object.values(THEME_KEYS).includes(k))) {
|
||||||
await refreshTheme();
|
await refreshTheme();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e.message || t('Save failed'));
|
message.error(e.message || t('Save failed'));
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 离开“外观设置”时,恢复后端持久化配置(取消未保存的预览)
|
// 离开“外观设置”时,恢复后端持久化配置(取消未保存的预览)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Alert, Button, Divider, Form, Input, Select, Switch, message } from 'an
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { rolesApi, type RoleInfo } from '../../../api/roles';
|
import { rolesApi, type RoleInfo } from '../../../api/roles';
|
||||||
import { useI18n } from '../../../i18n';
|
import { useI18n } from '../../../i18n';
|
||||||
|
import { normalizeLang, readStoredLang } from '../../../i18n/lang';
|
||||||
|
|
||||||
interface AppConfigKey {
|
interface AppConfigKey {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -12,7 +13,7 @@ interface AppConfigKey {
|
|||||||
interface AppSettingsTabProps {
|
interface AppSettingsTabProps {
|
||||||
config: Record<string, string>;
|
config: Record<string, string>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onSave: (values: Record<string, unknown>) => Promise<void>;
|
onSave: (values: Record<string, unknown>) => Promise<boolean>;
|
||||||
configKeys: AppConfigKey[];
|
configKeys: AppConfigKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export default function AppSettingsTab({
|
|||||||
onSave,
|
onSave,
|
||||||
configKeys,
|
configKeys,
|
||||||
}: AppSettingsTabProps) {
|
}: AppSettingsTabProps) {
|
||||||
const { t } = useI18n();
|
const { t, setLang } = useI18n();
|
||||||
const [rolesLoading, setRolesLoading] = useState(false);
|
const [rolesLoading, setRolesLoading] = useState(false);
|
||||||
const [roles, setRoles] = useState<RoleInfo[]>([]);
|
const [roles, setRoles] = useState<RoleInfo[]>([]);
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ export default function AppSettingsTab({
|
|||||||
const roleId = roleIdRaw ? Number(roleIdRaw) : undefined;
|
const roleId = roleIdRaw ? Number(roleIdRaw) : undefined;
|
||||||
return {
|
return {
|
||||||
...Object.fromEntries(configKeys.map(({ key, default: def }) => [key, config[key] ?? def ?? ''])),
|
...Object.fromEntries(configKeys.map(({ key, default: def }) => [key, config[key] ?? def ?? ''])),
|
||||||
|
APP_DEFAULT_LANGUAGE: normalizeLang(config.APP_DEFAULT_LANGUAGE, 'zh'),
|
||||||
AUTH_ALLOW_REGISTER: allowRegister,
|
AUTH_ALLOW_REGISTER: allowRegister,
|
||||||
AUTH_DEFAULT_REGISTER_ROLE_ID: Number.isFinite(roleId) ? roleId : undefined,
|
AUTH_DEFAULT_REGISTER_ROLE_ID: Number.isFinite(roleId) ? roleId : undefined,
|
||||||
};
|
};
|
||||||
@@ -66,12 +68,17 @@ export default function AppSettingsTab({
|
|||||||
for (const { key } of configKeys) {
|
for (const { key } of configKeys) {
|
||||||
payload[key] = vals[key];
|
payload[key] = vals[key];
|
||||||
}
|
}
|
||||||
|
const defaultLanguage = normalizeLang(vals.APP_DEFAULT_LANGUAGE, 'zh');
|
||||||
|
payload.APP_DEFAULT_LANGUAGE = defaultLanguage;
|
||||||
const allow = !!vals.AUTH_ALLOW_REGISTER;
|
const allow = !!vals.AUTH_ALLOW_REGISTER;
|
||||||
payload.AUTH_ALLOW_REGISTER = allow ? 'true' : 'false';
|
payload.AUTH_ALLOW_REGISTER = allow ? 'true' : 'false';
|
||||||
if (allow) {
|
if (allow) {
|
||||||
payload.AUTH_DEFAULT_REGISTER_ROLE_ID = String(vals.AUTH_DEFAULT_REGISTER_ROLE_ID);
|
payload.AUTH_DEFAULT_REGISTER_ROLE_ID = String(vals.AUTH_DEFAULT_REGISTER_ROLE_ID);
|
||||||
}
|
}
|
||||||
await onSave(payload);
|
const saved = await onSave(payload);
|
||||||
|
if (saved && !readStoredLang()) {
|
||||||
|
setLang(defaultLanguage, { persist: false });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
style={{ marginTop: 24 }}
|
style={{ marginTop: 24 }}
|
||||||
key={JSON.stringify(config)}
|
key={JSON.stringify(config)}
|
||||||
@@ -82,6 +89,20 @@ export default function AppSettingsTab({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="APP_DEFAULT_LANGUAGE"
|
||||||
|
label={t('Default Language')}
|
||||||
|
extra={t('Used when the user has not selected a language')}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
size="large"
|
||||||
|
options={[
|
||||||
|
{ value: 'zh', label: t('Chinese') },
|
||||||
|
{ value: 'en', label: t('English') },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Divider titlePlacement="left">{t('Registration Settings')}</Divider>
|
<Divider titlePlacement="left">{t('Registration Settings')}</Divider>
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface ThemeKeyMap {
|
|||||||
interface AppearanceSettingsTabProps {
|
interface AppearanceSettingsTabProps {
|
||||||
config: Record<string, string>;
|
config: Record<string, string>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onSave: (values: Record<string, unknown>) => Promise<void>;
|
onSave: (values: Record<string, unknown>) => Promise<boolean>;
|
||||||
themeKeys: ThemeKeyMap;
|
themeKeys: ThemeKeyMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
interface EmailSettingsTabProps {
|
interface EmailSettingsTabProps {
|
||||||
config: Record<string, string>;
|
config: Record<string, string>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onSave: (values: Record<string, unknown>) => Promise<void>;
|
onSave: (values: Record<string, unknown>) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmailFormValues {
|
interface EmailFormValues {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useI18n } from '../../../i18n';
|
|||||||
interface ProtocolMappingsTabProps {
|
interface ProtocolMappingsTabProps {
|
||||||
config: Record<string, string>;
|
config: Record<string, string>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onSave: (values: Record<string, unknown>) => Promise<void>;
|
onSave: (values: Record<string, unknown>) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WEBDAV_KEY = 'WEBDAV_MAPPING_ENABLED';
|
const WEBDAV_KEY = 'WEBDAV_MAPPING_ENABLED';
|
||||||
|
|||||||
+10
-2
@@ -7,12 +7,14 @@ import type { PluginItem } from './api/plugins';
|
|||||||
import { pluginsApi } from './api/plugins';
|
import { pluginsApi } from './api/plugins';
|
||||||
import request from './api/client';
|
import request from './api/client';
|
||||||
import { vfsApi, type VfsEntry } from './api/vfs';
|
import { vfsApi, type VfsEntry } from './api/vfs';
|
||||||
|
import { parseLang } from './i18n/lang';
|
||||||
|
|
||||||
type FrameMode = 'file' | 'app';
|
type FrameMode = 'file' | 'app';
|
||||||
|
|
||||||
type FrameQuery = {
|
type FrameQuery = {
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
mode: FrameMode;
|
mode: FrameMode;
|
||||||
|
lang: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
pluginVersion: string;
|
pluginVersion: string;
|
||||||
pluginStyles: string[] | null;
|
pluginStyles: string[] | null;
|
||||||
@@ -65,6 +67,7 @@ function getQuery(): FrameQuery {
|
|||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const pluginKey = (params.get('pluginKey') || '').trim();
|
const pluginKey = (params.get('pluginKey') || '').trim();
|
||||||
const mode = (params.get('mode') || 'file') as FrameMode;
|
const mode = (params.get('mode') || 'file') as FrameMode;
|
||||||
|
const lang = (params.get('lang') || '').trim();
|
||||||
const filePath = (params.get('filePath') || '').trim();
|
const filePath = (params.get('filePath') || '').trim();
|
||||||
const pluginVersion = (params.get('pluginVersion') || '').trim();
|
const pluginVersion = (params.get('pluginVersion') || '').trim();
|
||||||
|
|
||||||
@@ -88,7 +91,7 @@ function getQuery(): FrameQuery {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return { pluginKey, mode, filePath, pluginVersion, pluginStyles, entry };
|
return { pluginKey, mode, lang, filePath, pluginVersion, pluginStyles, entry };
|
||||||
}
|
}
|
||||||
|
|
||||||
function postToParent(data: any) {
|
function postToParent(data: any) {
|
||||||
@@ -279,9 +282,14 @@ async function buildFileContext(filePath: string, entryOverride: VfsEntry | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const query = getQuery();
|
||||||
|
const frameLang = parseLang(query.lang);
|
||||||
|
if (frameLang) {
|
||||||
|
document.documentElement.lang = frameLang;
|
||||||
|
}
|
||||||
initExternals();
|
initExternals();
|
||||||
|
|
||||||
const { pluginKey, mode, filePath, pluginVersion, pluginStyles, entry } = getQuery();
|
const { pluginKey, mode, filePath, pluginVersion, pluginStyles, entry } = query;
|
||||||
if (!pluginKey) {
|
if (!pluginKey) {
|
||||||
renderStatus('Missing pluginKey in query string', true);
|
renderStatus('Missing pluginKey in query string', true);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import { pluginsApi } from '../api/plugins';
|
|||||||
// 类型定义
|
// 类型定义
|
||||||
import type { VfsEntry, DirListing } from '../api/client';
|
import type { VfsEntry, DirListing } from '../api/client';
|
||||||
import type { PluginItem } from '../api/plugins';
|
import type { PluginItem } from '../api/plugins';
|
||||||
|
import { getActiveLang, normalizeLang, type Lang } from '../i18n/lang';
|
||||||
type Lang = 'zh' | 'en';
|
|
||||||
type Dict = Record<string, string>;
|
type Dict = Record<string, string>;
|
||||||
type Dicts = Partial<Record<Lang, Dict>>;
|
type Dicts = Partial<Record<Lang, Dict>>;
|
||||||
|
|
||||||
@@ -197,10 +196,8 @@ declare global {
|
|||||||
* 初始化并暴露外部依赖
|
* 初始化并暴露外部依赖
|
||||||
*/
|
*/
|
||||||
export function initExternals(): void {
|
export function initExternals(): void {
|
||||||
const normalizeLang = (raw: unknown): Lang => (raw === 'en' ? 'en' : 'zh');
|
|
||||||
|
|
||||||
const i18nApi = {
|
const i18nApi = {
|
||||||
getLang: () => normalizeLang(localStorage.getItem('lang')),
|
getLang: () => getActiveLang(),
|
||||||
subscribe: (cb: (lang: Lang) => void) => {
|
subscribe: (cb: (lang: Lang) => void) => {
|
||||||
const handler = (e: Event) => {
|
const handler = (e: Event) => {
|
||||||
const lang = (e as CustomEvent)?.detail?.lang as Lang;
|
const lang = (e as CustomEvent)?.detail?.lang as Lang;
|
||||||
|
|||||||
Reference in New Issue
Block a user