Save selected app language on server

This commit is contained in:
schroda
2026-01-22 00:12:22 +01:00
parent 94ee49f3ca
commit bb36a46a24
9 changed files with 58 additions and 30 deletions

View File

@@ -33,6 +33,8 @@ import { ISourceMetadata } from '@/features/source/Source.types.ts';
import { LibraryOptions } from '@/features/library/Library.types.ts';
import { MetadataThemeSettings } from '@/features/theme/AppTheme.types.ts';
import { TapZoneInvertMode } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
import { detectLocale, getISOLanguage } from '@/lib/ISOLanguageUtil.ts';
import { i18nResources } from '@/i18n';
export const APP_METADATA_KEY_PREFIX = 'webUI';
@@ -374,6 +376,18 @@ export const APP_METADATA: Record<
excludedScanlators: {
convert: convertToObject<string[]>,
},
locale: {
convert: convertToString,
toConstrainedValue: (value: string) => {
const locale = getISOLanguage(value)?.isoCode;
if (!locale || !i18nResources.includes(locale)) {
return detectLocale();
}
return locale;
},
},
} as const;
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA);
@@ -446,6 +460,7 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
'isEnabled',
// themes
'locale',
'customThemes',
'mangaThumbnailBackdrop',
'mangaDynamicColorSchemes',

View File

@@ -30,6 +30,7 @@ import {
WebUiInterface,
} from '@/lib/graphql/generated/graphql.ts';
import { ThemeMode } from '@/features/theme/AppTheme.types.ts';
import { getPreferredISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts';
export const MANGA_GRID_WIDTH = {
min: 100,
@@ -85,6 +86,7 @@ export const SERVER_SETTINGS_METADATA_DEFAULT: MetadataServerSettings = {
serverInformAvailableUpdate: true,
// themes
locale: getPreferredISOLanguageCodes()[0],
appTheme: 'default',
themeMode: ThemeMode.SYSTEM,
shouldUsePureBlackMode: false,

View File

@@ -19,7 +19,7 @@ import { useAppThemeContext } from '@/features/theme/AppThemeContext.tsx';
import { Select } from '@/base/components/inputs/Select.tsx';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { I18nResourceCode, i18nResources, loadCatalog } from '@/i18n';
import { i18nResources, loadCatalog } from '@/i18n';
import { languageCodeToName } from '@/base/utils/Languages.ts';
import { ThemeList } from '@/features/theme/components/ThemeList.tsx';
import {
@@ -38,7 +38,7 @@ import { MANGA_GRID_WIDTH, SERVER_SETTINGS_METADATA_DEFAULT } from '@/features/s
import { MUI_THEME_MODE_KEY } from '@/lib/mui/MUI.constants.ts';
export const Appearance = () => {
const { t, i18n } = useLingui();
const { t } = useLingui();
const { themeMode, setThemeMode, shouldUsePureBlackMode, setShouldUsePureBlackMode } = useAppThemeContext();
const { mode, setMode } = useColorScheme();
const actualThemeMode = (mode ?? themeMode) as ThemeMode;
@@ -46,7 +46,7 @@ export const Appearance = () => {
useAppTitle(t`Appearance`);
const {
settings: { mangaThumbnailBackdrop, mangaDynamicColorSchemes, mangaGridItemWidth },
settings: { mangaThumbnailBackdrop, mangaDynamicColorSchemes, mangaGridItemWidth, locale },
request: { loading, error, refetch },
} = useMetadataServerSettings();
const updateMetadataSetting = createUpdateMetadataServerSettings<keyof MetadataThemeSettings>((e) =>
@@ -135,12 +135,15 @@ export const Appearance = () => {
}
/>
<Select
value={i18nResources.includes(i18n.locale as I18nResourceCode) ? i18n.locale : 'en'}
onChange={({ target: { value: language } }) =>
loadCatalog(language).catch((e: Error) => {
value={locale}
onChange={({ target: { value: newLocale } }) => {
updateMetadataSetting('locale', newLocale).catch((e) =>
makeToast(t`Could not save changes`, 'error', getErrorMessage(e)),
);
loadCatalog(newLocale).catch((e: Error) => {
makeToast(t`Could not load language`, 'error', getErrorMessage(e));
})
}
});
}}
>
{i18nResources.map((language) => (
<MenuItem key={language} value={language}>

View File

@@ -29,6 +29,7 @@ export type TAppThemeContext = {
>;
};
export type MetadataThemeSettings = {
locale: string;
appTheme: AppThemes;
themeMode: ThemeMode;
shouldUsePureBlackMode: boolean;

View File

@@ -25,6 +25,8 @@ import { AppStorage } from '@/lib/storage/AppStorage.ts';
import { DIRECTION_TO_CACHE } from '@/features/theme/ThemeDirectionCache.ts';
import { TAppThemeContext, ThemeMode } from '@/features/theme/AppTheme.types.ts';
import { getLanguageReadingDirection } from '@/lib/ISOLanguageUtil.ts';
import { loadCatalog } from '@/i18n';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
export const AppThemeContext = React.createContext<TAppThemeContext>({
appTheme: 'default',
@@ -40,10 +42,10 @@ export const AppThemeContext = React.createContext<TAppThemeContext>({
export const useAppThemeContext = () => useContext(AppThemeContext);
export const AppThemeContextProvider = ({ children }: { children: ReactNode }) => {
const { t, i18n } = useLingui();
const { t } = useLingui();
const {
request: metadataServerSettingsRequest,
settings: { appTheme: serverAppTheme, themeMode, shouldUsePureBlackMode, customThemes },
settings: { appTheme: serverAppTheme, themeMode, shouldUsePureBlackMode, customThemes, locale },
} = useMetadataServerSettings();
const [localAppTheme, setLocalAppTheme] = useLocalStorage<AppTheme>(
'appTheme',
@@ -61,7 +63,7 @@ export const AppThemeContextProvider = ({ children }: { children: ReactNode }) =
const appTheme = areMetadataServerSettingsReady ? serverAppTheme : localAppTheme.id;
const actualThemeMode = areMetadataServerSettingsReady ? themeMode : localThemeMode;
const currentDirection = getLanguageReadingDirection(i18n.locale);
const currentDirection = getLanguageReadingDirection(locale);
const updateSetting = createUpdateMetadataServerSettings<'appTheme' | 'themeMode' | 'shouldUsePureBlackMode'>((e) =>
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)),
@@ -108,6 +110,14 @@ export const AppThemeContextProvider = ({ children }: { children: ReactNode }) =
return () => unsubscribe();
}, []);
useEffect(() => {
if (!areMetadataServerSettingsReady) {
return;
}
loadCatalog(locale).catch(defaultPromiseErrorHandler('AppThemeContextProvider::loadCatalog'));
}, [areMetadataServerSettingsReady]);
useEffect(() => {
if (!areMetadataServerSettingsReady) {
return;

View File

@@ -7,8 +7,7 @@
*/
import { i18n } from '@lingui/core';
import { DEFAULT_LANGUAGE, getISOLanguage, getPreferredISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts';
import { AppStorage } from '@/lib/storage/AppStorage.ts';
import { detectLocale } from '@/lib/ISOLanguageUtil.ts';
import localesConfig from '@/i18n/locales.json';
export const i18nResources = localesConfig.locales;
@@ -17,28 +16,11 @@ export type I18nResourceCode = (typeof i18nResources)[number];
const DEFAULT_LOCALE = localesConfig.defaultLocale;
const SELECTED_LANGUAGE_CODE_KEY = 'i18n:selected-language';
export async function loadCatalog(locale: string): Promise<void> {
const localeToLoad = i18nResources.includes(locale as (typeof i18nResources)[number]) ? locale : DEFAULT_LOCALE;
const { messages } = await import(`./locales/${localeToLoad}.po`);
i18n.loadAndActivate({ locale: localeToLoad, messages });
AppStorage.local.setItem(SELECTED_LANGUAGE_CODE_KEY, localeToLoad);
}
function detectLocale(): string {
const storedLanguage = AppStorage.local.getItem(SELECTED_LANGUAGE_CODE_KEY);
const preferredLanguages = getPreferredISOLanguageCodes();
const languageCodes = [storedLanguage, ...preferredLanguages];
const normalizedLanguageCodes = languageCodes
.filter((code) => code !== null)
.filter((code) => getISOLanguage(code))
.filter((code) => i18nResources.includes(code as (typeof i18nResources)[number]));
return normalizedLanguageCodes[0] ?? DEFAULT_LANGUAGE;
}
export async function initializeLocalization(): Promise<void> {

View File

@@ -985,6 +985,10 @@ msgstr "Could not restore backup"
msgid "Could not retry failed download."
msgstr "Could not retry failed download."
#: src/features/settings/screens/Appearance.tsx
msgid "Could not save changes"
msgstr "Could not save changes"
#: src/features/category/components/CategorySelect.tsx
#: src/features/library/components/LibraryOptionsPanel.tsx
#: src/features/library/screens/LibrarySettings.tsx

View File

@@ -7,6 +7,7 @@
*/
import { ISOLanguage, IsoLanguages } from '@/base/IsoLanguages.ts';
import { i18nResources } from '@/i18n';
export type LanguageObject = ISOLanguage & { orgCode: string; isoCode: string };
@@ -137,3 +138,12 @@ export function getLanguageReadingDirection(code: string): 'ltr' | 'rtl' {
return 'ltr';
}
export function detectLocale(): string {
const normalizedLanguageCodes = getPreferredISOLanguageCodes()
.filter((code) => code !== null)
.filter((code) => getISOLanguage(code))
.filter((code) => i18nResources.includes(code as (typeof i18nResources)[number]));
return normalizedLanguageCodes[0] ?? DEFAULT_LANGUAGE;
}