mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Handle restricted language configuration fallback V2 (#5154)
## Summary - restrict supported languages to the validated list from app-config instead of always adding an extra fallback - set the effective fallback locale to the preferred configured language and switch away from disallowed selections automatically ## Testing - ./gradlew build ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_6930529bc6c08328a1ce05f7d1316e27)
This commit is contained in:
+41
-23
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge, SegmentedControl } from '@mantine/core';
|
||||
import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge, SegmentedControl, Select } from '@mantine/core';
|
||||
import { alert } from '@app/components/toast';
|
||||
import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal';
|
||||
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
|
||||
@@ -11,6 +11,8 @@ import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
import { usePreferences } from '@app/contexts/PreferencesContext';
|
||||
import { useUnsavedChanges } from '@app/contexts/UnsavedChangesContext';
|
||||
import { supportedLanguages, toUnderscoreFormat, toUnderscoreLanguages } from '@app/i18n';
|
||||
import { Z_INDEX_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
|
||||
interface GeneralSettingsData {
|
||||
ui: {
|
||||
@@ -49,6 +51,12 @@ export default function AdminGeneralSection() {
|
||||
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
|
||||
const { preferences, updatePreference } = usePreferences();
|
||||
const { setIsDirty, markClean } = useUnsavedChanges();
|
||||
const languageOptions = useMemo(
|
||||
() => Object.entries(supportedLanguages)
|
||||
.map(([code, label]) => ({ value: toUnderscoreFormat(code), label: `${label} (${code})` }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
[]
|
||||
);
|
||||
|
||||
// Track original settings for dirty detection
|
||||
const [originalSettingsSnapshot, setOriginalSettingsSnapshot] = useState<string>('');
|
||||
@@ -73,9 +81,11 @@ export default function AdminGeneralSection() {
|
||||
apiClient.get('/api/v1/admin/settings/section/premium')
|
||||
]);
|
||||
|
||||
const ui = uiResponse.data || {};
|
||||
const system = systemResponse.data || {};
|
||||
const premium = premiumResponse.data || {};
|
||||
const ui = { ...(uiResponse.data || {}) };
|
||||
const system = { ...(systemResponse.data || {}) };
|
||||
const premium = { ...(premiumResponse.data || {}) };
|
||||
|
||||
ui.languages = Array.isArray(ui.languages) ? toUnderscoreLanguages(ui.languages) : [];
|
||||
|
||||
const result: any = {
|
||||
ui,
|
||||
@@ -152,6 +162,21 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
});
|
||||
|
||||
const selectedLanguages = useMemo(
|
||||
() => toUnderscoreLanguages(settings.ui?.languages || []),
|
||||
[settings.ui?.languages]
|
||||
);
|
||||
|
||||
// Filter default locale options based on available languages setting
|
||||
const defaultLocaleOptions = useMemo(() => {
|
||||
// If no languages are selected (empty), show all languages
|
||||
if (!selectedLanguages || selectedLanguages.length === 0) {
|
||||
return languageOptions;
|
||||
}
|
||||
// Otherwise, only show languages that are in the selected list
|
||||
return languageOptions.filter(option => selectedLanguages.includes(option.value));
|
||||
}, [selectedLanguages, languageOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch real settings if login is enabled
|
||||
if (loginEnabled) {
|
||||
@@ -369,30 +394,19 @@ export default function AdminGeneralSection() {
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.general.languages.description', 'Limit which languages are available (empty = all languages)')}
|
||||
value={settings.ui?.languages || []}
|
||||
value={selectedLanguages}
|
||||
onChange={(value) => setSettings({ ...settings, ui: { ...settings.ui, languages: value } })}
|
||||
data={[
|
||||
{ value: 'de_DE', label: 'Deutsch' },
|
||||
{ value: 'es_ES', label: 'Español' },
|
||||
{ value: 'fr_FR', label: 'Français' },
|
||||
{ value: 'it_IT', label: 'Italiano' },
|
||||
{ value: 'pl_PL', label: 'Polski' },
|
||||
{ value: 'pt_BR', label: 'Português (Brasil)' },
|
||||
{ value: 'ru_RU', label: 'Русский' },
|
||||
{ value: 'zh_CN', label: '简体中文' },
|
||||
{ value: 'ja_JP', label: '日本語' },
|
||||
{ value: 'ko_KR', label: '한국어' },
|
||||
]}
|
||||
data={languageOptions}
|
||||
searchable
|
||||
clearable
|
||||
placeholder={t('admin.settings.general.languages.placeholder', 'Select languages')}
|
||||
comboboxProps={{ zIndex: 1400 }}
|
||||
comboboxProps={{ zIndex: Z_INDEX_CONFIG_MODAL }}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
<Select
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.defaultLocale.label', 'Default Locale')}</span>
|
||||
@@ -400,9 +414,13 @@ export default function AdminGeneralSection() {
|
||||
</Group>
|
||||
}
|
||||
description={t('admin.settings.general.defaultLocale.description', 'The default language for new users (e.g., en_US, es_ES)')}
|
||||
value={ settings.system?.defaultLocale || ''}
|
||||
onChange={(e) => setSettings({ ...settings, system: { ...settings.system, defaultLocale: e.target.value } })}
|
||||
placeholder="en_US"
|
||||
value={settings.system?.defaultLocale || ''}
|
||||
onChange={(value) => setSettings({ ...settings, system: { ...settings.system, defaultLocale: value || '' } })}
|
||||
data={defaultLocaleOptions}
|
||||
searchable
|
||||
clearable
|
||||
placeholder="en_GB"
|
||||
comboboxProps={{ zIndex: Z_INDEX_CONFIG_MODAL }}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,22 @@ vi.mock('react-i18next', () => ({
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: '3rdParty',
|
||||
init: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock i18n module to avoid initialization
|
||||
vi.mock('@app/i18n', () => ({
|
||||
updateSupportedLanguages: vi.fn(),
|
||||
supportedLanguages: { 'en-GB': 'English' },
|
||||
rtlLanguages: [],
|
||||
default: {
|
||||
language: 'en-GB',
|
||||
changeLanguage: vi.fn(),
|
||||
options: {},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock useAuth hook
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useBackendProbe } from '@app/hooks/useBackendProbe';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
import { type OAuthProvider } from '@app/auth/oauthTypes';
|
||||
import { updateSupportedLanguages } from '@app/i18n';
|
||||
|
||||
// Import login components
|
||||
import LoginHeader from '@app/routes/login/LoginHeader';
|
||||
@@ -103,6 +104,11 @@ export default function Login() {
|
||||
setIsFirstTimeSetup(data.firstTimeSetup ?? false);
|
||||
setShowDefaultCredentials(data.showDefaultCredentials ?? false);
|
||||
|
||||
// Apply language configuration from server
|
||||
if (data.languages || data.defaultLocale) {
|
||||
updateSupportedLanguages(data.languages, data.defaultLocale);
|
||||
}
|
||||
|
||||
// Extract provider IDs from the providerList map
|
||||
// The keys are like "/oauth2/authorization/google" - extract the last part
|
||||
const providerIds = Object.keys(data.providerList || {})
|
||||
|
||||
Reference in New Issue
Block a user