mirror of
https://github.com/inventree/InvenTree.git
synced 2026-02-25 16:17:58 +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:
@@ -291,3 +291,11 @@ Users may opt to disable the spotlight search functionality if they do not find
|
|||||||
Many aspects of the user interface are controlled by user permissions, which determine what actions and features are available to each user based on their assigned roles and permissions within the system. This allows for a highly customizable user experience, where different users can have access to different features and functionality based on their specific needs and responsibilities within the organization.
|
Many aspects of the user interface are controlled by user permissions, which determine what actions and features are available to each user based on their assigned roles and permissions within the system. This allows for a highly customizable user experience, where different users can have access to different features and functionality based on their specific needs and responsibilities within the organization.
|
||||||
|
|
||||||
If a user does not have permission to access a particular feature or section of the system, that feature will be hidden from their view in the user interface. This helps to ensure that users only see the features and information that are relevant to their role, reducing clutter and improving usability.
|
If a user does not have permission to access a particular feature or section of the system, that feature will be hidden from their view in the user interface. This helps to ensure that users only see the features and information that are relevant to their role, reducing clutter and improving usability.
|
||||||
|
|
||||||
|
## Language Support
|
||||||
|
|
||||||
|
The InvenTree user interface supports multiple languages, allowing users to interact with the system in their preferred language.
|
||||||
|
|
||||||
|
The default system language can be configured by the system administrator in the [server configuration options](../start/config.md#basic-options).
|
||||||
|
|
||||||
|
Additionally, users can select their preferred language in their [user settings](../settings/user.md), allowing them to override the system default language with their own choice. This provides a personalized experience for each user, ensuring that they can interact with the system in the language they are most comfortable with.
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ import {
|
|||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import {
|
||||||
|
defaultLocale,
|
||||||
|
getPriorityLocale
|
||||||
|
} from '../../contexts/LanguageContext';
|
||||||
import type { CalendarState } from '../../hooks/UseCalendar';
|
import type { CalendarState } from '../../hooks/UseCalendar';
|
||||||
import { useLocalState } from '../../states/LocalState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer';
|
import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer';
|
||||||
@@ -60,7 +64,19 @@ export default function Calendar({
|
|||||||
const [locale] = useLocalState(useShallow((s) => [s.language]));
|
const [locale] = useLocalState(useShallow((s) => [s.language]));
|
||||||
|
|
||||||
// Ensure underscore is replaced with dash
|
// 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(
|
const selectMonth = useCallback(
|
||||||
(date: DateValue) => {
|
(date: DateValue) => {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Select } from '@mantine/core';
|
import { Select } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { getSupportedLanguages } from '../../contexts/LanguageContext';
|
import {
|
||||||
|
activateLocale,
|
||||||
|
getSupportedLanguages
|
||||||
|
} from '../../contexts/LanguageContext';
|
||||||
import { useLocalState } from '../../states/LocalState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
|
|
||||||
export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
|
export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
|
||||||
@@ -28,13 +32,21 @@ export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
|
|||||||
}));
|
}));
|
||||||
setLangOptions(newLangOptions);
|
setLangOptions(newLangOptions);
|
||||||
setValue(locale);
|
setValue(locale);
|
||||||
|
activateLocale(locale); // Ensure the locale is activated on component load
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
w={width}
|
w={width}
|
||||||
data={langOptions}
|
data={[
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: t`Default Language`
|
||||||
|
},
|
||||||
|
...langOptions
|
||||||
|
]}
|
||||||
value={value}
|
value={value}
|
||||||
|
defaultValue={''}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
searchable
|
searchable
|
||||||
aria-label='Select language'
|
aria-label='Select language'
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
type InvenTreePluginContext
|
type InvenTreePluginContext
|
||||||
} from '@lib/types/Plugins';
|
} from '@lib/types/Plugins';
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
|
import { defaultLocale } from '../../contexts/LanguageContext';
|
||||||
import {
|
import {
|
||||||
useAddStockItem,
|
useAddStockItem,
|
||||||
useAssignStockItem,
|
useAssignStockItem,
|
||||||
@@ -35,10 +36,12 @@ import {
|
|||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
|
import { useServerApiState } from '../../states/ServerApiState';
|
||||||
import { RenderInstance } from '../render/Instance';
|
import { RenderInstance } from '../render/Instance';
|
||||||
|
|
||||||
export const useInvenTreeContext = () => {
|
export const useInvenTreeContext = () => {
|
||||||
const [locale, host] = useLocalState(useShallow((s) => [s.language, s.host]));
|
const [locale, host] = useLocalState(useShallow((s) => [s.language, s.host]));
|
||||||
|
const [server] = useServerApiState(useShallow((s) => [s.server]));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
@@ -57,7 +60,7 @@ export const useInvenTreeContext = () => {
|
|||||||
user: user,
|
user: user,
|
||||||
host: host,
|
host: host,
|
||||||
i18n: i18n,
|
i18n: i18n,
|
||||||
locale: locale,
|
locale: locale || server.default_locale || defaultLocale,
|
||||||
api: api,
|
api: api,
|
||||||
queryClient: queryClient,
|
queryClient: queryClient,
|
||||||
navigate: navigate,
|
navigate: navigate,
|
||||||
|
|||||||
@@ -65,9 +65,29 @@ export function LanguageContext({
|
|||||||
const [language] = useLocalState(useShallow((state) => [state.language]));
|
const [language] = useLocalState(useShallow((state) => [state.language]));
|
||||||
const [server] = useServerApiState(useShallow((state) => [state.server]));
|
const [server] = useServerApiState(useShallow((state) => [state.server]));
|
||||||
|
|
||||||
|
const [activeLocale, setActiveLocale] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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<
|
const [loadedState, setLoadedState] = useState<
|
||||||
'loading' | 'loaded' | 'error'
|
'loading' | 'loaded' | 'error'
|
||||||
@@ -77,7 +97,7 @@ export function LanguageContext({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isMounted.current = true;
|
isMounted.current = true;
|
||||||
|
|
||||||
let lang = language;
|
let lang: string = language || defaultLocale;
|
||||||
|
|
||||||
// Ensure that the selected language is supported
|
// Ensure that the selected language is supported
|
||||||
if (!Object.keys(getSupportedLanguages()).includes(lang)) {
|
if (!Object.keys(getSupportedLanguages()).includes(lang)) {
|
||||||
@@ -96,7 +116,7 @@ export function LanguageContext({
|
|||||||
*/
|
*/
|
||||||
const locales: (string | undefined)[] = [];
|
const locales: (string | undefined)[] = [];
|
||||||
|
|
||||||
if (lang != 'pseudo-LOCALE') {
|
if (!!lang && lang != 'pseudo-LOCALE') {
|
||||||
locales.push(lang);
|
locales.push(lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +176,26 @@ export function LanguageContext({
|
|||||||
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
|
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function activateLocale(locale: string) {
|
// This function is used to determine the locale to activate based on the prioritization rules.
|
||||||
const { messages } = await import(`../locales/${locale}/messages.ts`);
|
export function getPriorityLocale(): string {
|
||||||
i18n.load(locale, messages);
|
const serverDefault = useServerApiState.getState().server.default_locale;
|
||||||
i18n.activate(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { Trans } from '@lingui/react';
|
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 { ModalsProvider } from '@mantine/modals';
|
||||||
import { Notifications } from '@mantine/notifications';
|
import { Notifications } from '@mantine/notifications';
|
||||||
import { ContextMenuProvider } from 'mantine-contextmenu';
|
import { ContextMenuProvider } from 'mantine-contextmenu';
|
||||||
@@ -20,23 +24,31 @@ export function ThemeContext({
|
|||||||
}: Readonly<{ children: JSX.Element }>) {
|
}: Readonly<{ children: JSX.Element }>) {
|
||||||
const [userTheme] = useLocalState(useShallow((state) => [state.userTheme]));
|
const [userTheme] = useLocalState(useShallow((state) => [state.userTheme]));
|
||||||
|
|
||||||
|
let customUserTheme: MantineThemeOverride | undefined = undefined;
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
const myTheme = createTheme({
|
try {
|
||||||
primaryColor: userTheme.primaryColor,
|
customUserTheme = createTheme({
|
||||||
white: userTheme.whiteColor,
|
primaryColor: userTheme.primaryColor,
|
||||||
black: userTheme.blackColor,
|
white: userTheme.whiteColor,
|
||||||
defaultRadius: userTheme.radius,
|
black: userTheme.blackColor,
|
||||||
breakpoints: {
|
defaultRadius: userTheme.radius,
|
||||||
xs: '30em',
|
breakpoints: {
|
||||||
sm: '48em',
|
xs: '30em',
|
||||||
md: '64em',
|
sm: '48em',
|
||||||
lg: '74em',
|
md: '64em',
|
||||||
xl: '90em'
|
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 (
|
return (
|
||||||
<MantineProvider theme={myTheme} colorSchemeManager={colorSchema}>
|
<MantineProvider theme={customUserTheme} colorSchemeManager={colorSchema}>
|
||||||
<ContextMenuProvider>
|
<ContextMenuProvider>
|
||||||
<LanguageContext>
|
<LanguageContext>
|
||||||
<ModalsProvider
|
<ModalsProvider
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ interface LocalStateProps {
|
|||||||
hostKey: string;
|
hostKey: string;
|
||||||
hostList: HostList;
|
hostList: HostList;
|
||||||
setHostList: (newHostList: HostList) => void;
|
setHostList: (newHostList: HostList) => void;
|
||||||
language: string;
|
language: string | null;
|
||||||
setLanguage: (newLanguage: string, noPatch?: boolean) => void;
|
setLanguage: (newLanguage: string | null, noPatch?: boolean) => void;
|
||||||
userTheme: UserTheme;
|
userTheme: UserTheme;
|
||||||
setTheme: (
|
setTheme: (
|
||||||
newValues: {
|
newValues: {
|
||||||
@@ -78,7 +78,7 @@ export const useLocalState = create<LocalStateProps>()(
|
|||||||
hostKey: '',
|
hostKey: '',
|
||||||
hostList: {},
|
hostList: {},
|
||||||
setHostList: (newHostList) => set({ hostList: newHostList }),
|
setHostList: (newHostList) => set({ hostList: newHostList }),
|
||||||
language: 'en',
|
language: null,
|
||||||
setLanguage: (newLanguage, noPatch = false) => {
|
setLanguage: (newLanguage, noPatch = false) => {
|
||||||
set({ language: newLanguage });
|
set({ language: newLanguage });
|
||||||
if (!noPatch) patchUser('language', newLanguage);
|
if (!noPatch) patchUser('language', newLanguage);
|
||||||
|
|||||||
Reference in New Issue
Block a user