Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/docs/concepts/user_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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.
18 changes: 17 additions & 1 deletion src/frontend/src/components/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand Down
16 changes: 14 additions & 2 deletions src/frontend/src/components/items/LanguageSelect.tsx
Original file line number Diff line number Diff line change
@@ -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 }>) {
Expand All @@ -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}
Comment on lines 38 to 50
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The select is controlled via value={value}, but when the stored locale is null you call setValue(locale) which leaves the control with null (no option selected). That means the new "Default Language" option (value '') is never shown as selected, and defaultValue has no effect in controlled mode. Consider controlling the select with value={locale ?? ''} and converting '' -> null in onChange so the UI reflects the default-language state.

Copilot uses AI. Check for mistakes.
searchable
aria-label='Select language'
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/src/components/plugins/PluginContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type InvenTreePluginContext
} from '@lib/types/Plugins';
import { i18n } from '@lingui/core';
import { defaultLocale } from '../../contexts/LanguageContext';
import {
useAddStockItem,
useAssignStockItem,
Expand All @@ -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();
Expand All @@ -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,
Expand Down
54 changes: 46 additions & 8 deletions src/frontend/src/contexts/LanguageContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)) {
Expand All @@ -96,7 +116,7 @@ export function LanguageContext({
*/
const locales: (string | undefined)[] = [];

if (lang != 'pseudo-LOCALE') {
if (!!lang && lang != 'pseudo-LOCALE') {
locales.push(lang);
}

Expand Down Expand Up @@ -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);
}
}
42 changes: 27 additions & 15 deletions src/frontend/src/contexts/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/frontend/src/states/LocalState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down
Loading