2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-03-04 03:11:46 +00:00

[UI] Default locale (#11412)

* [UI] Support default server language

* Handle faulty theme

* Add option for default language

* Improve language selection

* Brief docs entry

* Fix typo

* Fix yarn build

* Remove debug msg

* Fix calendar locale
This commit is contained in:
Oliver
2026-02-24 16:08:26 +11:00
committed by GitHub
parent 246108e732
commit 8a4ad4ff62
7 changed files with 119 additions and 30 deletions

View File

@@ -29,6 +29,10 @@ import {
} from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
defaultLocale,
getPriorityLocale
} from '../../contexts/LanguageContext';
import type { CalendarState } from '../../hooks/UseCalendar';
import { useLocalState } from '../../states/LocalState';
import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer';
@@ -60,7 +64,19 @@ export default function Calendar({
const [locale] = useLocalState(useShallow((s) => [s.language]));
// Ensure underscore is replaced with dash
const calendarLocale = useMemo(() => locale.replace('_', '-'), [locale]);
const calendarLocale = useMemo(() => {
let _locale: string | null = locale;
if (!_locale) {
_locale = getPriorityLocale();
}
_locale = _locale || defaultLocale;
_locale = _locale.replace('_', '-');
return _locale;
}, [locale]);
const selectMonth = useCallback(
(date: DateValue) => {

View File

@@ -1,8 +1,12 @@
import { Select } from '@mantine/core';
import { useEffect, useState } from 'react';
import { t } from '@lingui/core/macro';
import { useShallow } from 'zustand/react/shallow';
import { getSupportedLanguages } from '../../contexts/LanguageContext';
import {
activateLocale,
getSupportedLanguages
} from '../../contexts/LanguageContext';
import { useLocalState } from '../../states/LocalState';
export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
@@ -28,13 +32,21 @@ export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
}));
setLangOptions(newLangOptions);
setValue(locale);
activateLocale(locale); // Ensure the locale is activated on component load
}, [locale]);
return (
<Select
w={width}
data={langOptions}
data={[
{
value: '',
label: t`Default Language`
},
...langOptions
]}
value={value}
defaultValue={''}
onChange={setValue}
searchable
aria-label='Select language'

View File

@@ -18,6 +18,7 @@ import {
type InvenTreePluginContext
} from '@lib/types/Plugins';
import { i18n } from '@lingui/core';
import { defaultLocale } from '../../contexts/LanguageContext';
import {
useAddStockItem,
useAssignStockItem,
@@ -35,10 +36,12 @@ import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useServerApiState } from '../../states/ServerApiState';
import { RenderInstance } from '../render/Instance';
export const useInvenTreeContext = () => {
const [locale, host] = useLocalState(useShallow((s) => [s.language, s.host]));
const [server] = useServerApiState(useShallow((s) => [s.server]));
const navigate = useNavigate();
const user = useUserState();
const { colorScheme } = useMantineColorScheme();
@@ -57,7 +60,7 @@ export const useInvenTreeContext = () => {
user: user,
host: host,
i18n: i18n,
locale: locale,
locale: locale || server.default_locale || defaultLocale,
api: api,
queryClient: queryClient,
navigate: navigate,

View File

@@ -65,9 +65,29 @@ export function LanguageContext({
const [language] = useLocalState(useShallow((state) => [state.language]));
const [server] = useServerApiState(useShallow((state) => [state.server]));
const [activeLocale, setActiveLocale] = useState<string | null>(null);
useEffect(() => {
activateLocale(defaultLocale);
}, []);
// Update the locale based on prioritization:
// 1. Locally selected locale
// 2. Server default locale
// 3. English (fallback)
let locale: string | null = activeLocale;
if (!!language) {
locale = language;
} else if (!!server.default_locale) {
locale = server.default_locale;
} else {
locale = defaultLocale;
}
if (locale != activeLocale) {
setActiveLocale(locale);
activateLocale(locale);
}
}, [activeLocale, language, server.default_locale, defaultLocale]);
const [loadedState, setLoadedState] = useState<
'loading' | 'loaded' | 'error'
@@ -77,7 +97,7 @@ export function LanguageContext({
useEffect(() => {
isMounted.current = true;
let lang = language;
let lang: string = language || defaultLocale;
// Ensure that the selected language is supported
if (!Object.keys(getSupportedLanguages()).includes(lang)) {
@@ -96,7 +116,7 @@ export function LanguageContext({
*/
const locales: (string | undefined)[] = [];
if (lang != 'pseudo-LOCALE') {
if (!!lang && lang != 'pseudo-LOCALE') {
locales.push(lang);
}
@@ -156,8 +176,26 @@ export function LanguageContext({
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
}
export async function activateLocale(locale: string) {
const { messages } = await import(`../locales/${locale}/messages.ts`);
i18n.load(locale, messages);
i18n.activate(locale);
// This function is used to determine the locale to activate based on the prioritization rules.
export function getPriorityLocale(): string {
const serverDefault = useServerApiState.getState().server.default_locale;
const userDefault = useLocalState.getState().language;
return userDefault || serverDefault || defaultLocale;
}
export async function activateLocale(locale: string | null) {
if (!locale) {
locale = getPriorityLocale();
}
const localeDir = locale.split('-')[0]; // Extract the base locale (e.g., 'en' from 'en-US')
try {
const { messages } = await import(`../locales/${localeDir}/messages.ts`);
i18n.load(locale, messages);
i18n.activate(locale);
} catch (err) {
console.error(`Failed to load locale ${locale}:`, err);
}
}

View File

@@ -1,6 +1,10 @@
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react';
import { MantineProvider, createTheme } from '@mantine/core';
import {
MantineProvider,
type MantineThemeOverride,
createTheme
} from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
import { ContextMenuProvider } from 'mantine-contextmenu';
@@ -20,23 +24,31 @@ export function ThemeContext({
}: Readonly<{ children: JSX.Element }>) {
const [userTheme] = useLocalState(useShallow((state) => [state.userTheme]));
let customUserTheme: MantineThemeOverride | undefined = undefined;
// Theme
const myTheme = createTheme({
primaryColor: userTheme.primaryColor,
white: userTheme.whiteColor,
black: userTheme.blackColor,
defaultRadius: userTheme.radius,
breakpoints: {
xs: '30em',
sm: '48em',
md: '64em',
lg: '74em',
xl: '90em'
}
});
try {
customUserTheme = createTheme({
primaryColor: userTheme.primaryColor,
white: userTheme.whiteColor,
black: userTheme.blackColor,
defaultRadius: userTheme.radius,
breakpoints: {
xs: '30em',
sm: '48em',
md: '64em',
lg: '74em',
xl: '90em'
}
});
} catch (error) {
console.error('Error creating theme with user settings:', error);
// Fallback to default theme if there's an error
customUserTheme = undefined;
}
return (
<MantineProvider theme={myTheme} colorSchemeManager={colorSchema}>
<MantineProvider theme={customUserTheme} colorSchemeManager={colorSchema}>
<ContextMenuProvider>
<LanguageContext>
<ModalsProvider

View File

@@ -17,8 +17,8 @@ interface LocalStateProps {
hostKey: string;
hostList: HostList;
setHostList: (newHostList: HostList) => void;
language: string;
setLanguage: (newLanguage: string, noPatch?: boolean) => void;
language: string | null;
setLanguage: (newLanguage: string | null, noPatch?: boolean) => void;
userTheme: UserTheme;
setTheme: (
newValues: {
@@ -78,7 +78,7 @@ export const useLocalState = create<LocalStateProps>()(
hostKey: '',
hostList: {},
setHostList: (newHostList) => set({ hostList: newHostList }),
language: 'en',
language: null,
setLanguage: (newLanguage, noPatch = false) => {
set({ language: newLanguage });
if (!noPatch) patchUser('language', newLanguage);