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
2 changes: 1 addition & 1 deletion invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"common": {
"hotkeysLabel": "Hotkeys",
"themeLabel": "Theme",
"languagePickerLabel": "Language Picker",
"languagePickerLabel": "Language",
"reportBugLabel": "Report Bug",
"githubLabel": "Github",
"discordLabel": "Discord",
Expand Down
11 changes: 10 additions & 1 deletion invokeai/frontend/web/src/app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Box, Flex, Grid, Portal } from '@chakra-ui/react';
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { memo, ReactNode, useCallback, useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Loading from 'common/components/Loading/Loading';
Expand All @@ -22,6 +22,8 @@ import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger';
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n';

const DEFAULT_CONFIG = {};

Expand All @@ -33,6 +35,9 @@ interface Props {
const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
useToastWatcher();
useGlobalHotkeys();

const language = useAppSelector(languageSelector);

const log = useLogger();

const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
Expand All @@ -43,6 +48,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {

const dispatch = useAppDispatch();

useEffect(() => {
i18n.changeLanguage(language);
}, [language]);

useEffect(() => {
log.info({ namespace: 'App', data: config }, 'Received config');
dispatch(configChanged(config));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,69 @@
import type { ReactNode } from 'react';

import { VStack } from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import {
IconButton,
Menu,
MenuButton,
MenuItemOption,
MenuList,
MenuOptionGroup,
Tooltip,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaLanguage } from 'react-icons/fa';

export default function LanguagePicker() {
const { t, i18n } = useTranslation();
const LANGUAGES = {
ar: t('common.langArabic', { lng: 'ar' }),
nl: t('common.langDutch', { lng: 'nl' }),
en: t('common.langEnglish', { lng: 'en' }),
fr: t('common.langFrench', { lng: 'fr' }),
de: t('common.langGerman', { lng: 'de' }),
he: t('common.langHebrew', { lng: 'he' }),
it: t('common.langItalian', { lng: 'it' }),
ja: t('common.langJapanese', { lng: 'ja' }),
ko: t('common.langKorean', { lng: 'ko' }),
pl: t('common.langPolish', { lng: 'pl' }),
pt_BR: t('common.langBrPortuguese', { lng: 'pt_BR' }),
pt: t('common.langPortuguese', { lng: 'pt' }),
ru: t('common.langRussian', { lng: 'ru' }),
zh_CN: t('common.langSimplifiedChinese', { lng: 'zh_CN' }),
es: t('common.langSpanish', { lng: 'es' }),
uk: t('common.langUkranian', { lng: 'ua' }),
};
import i18n from 'i18n';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { languageSelector } from '../store/systemSelectors';
import { languageChanged } from '../store/systemSlice';
import { map } from 'lodash-es';
import { IoLanguage } from 'react-icons/io5';

const renderLanguagePicker = () => {
const languagesToRender: ReactNode[] = [];
Object.keys(LANGUAGES).forEach((lang) => {
languagesToRender.push(
<IAIButton
key={lang}
isChecked={localStorage.getItem('i18nextLng') === lang}
leftIcon={
localStorage.getItem('i18nextLng') === lang ? (
<FaCheck />
) : undefined
}
onClick={() => i18n.changeLanguage(lang)}
aria-label={LANGUAGES[lang as keyof typeof LANGUAGES]}
size="sm"
minWidth="200px"
>
{LANGUAGES[lang as keyof typeof LANGUAGES]}
</IAIButton>
);
});
export const LANGUAGES = {
ar: i18n.t('common.langArabic', { lng: 'ar' }),
nl: i18n.t('common.langDutch', { lng: 'nl' }),
en: i18n.t('common.langEnglish', { lng: 'en' }),
fr: i18n.t('common.langFrench', { lng: 'fr' }),
de: i18n.t('common.langGerman', { lng: 'de' }),
he: i18n.t('common.langHebrew', { lng: 'he' }),
it: i18n.t('common.langItalian', { lng: 'it' }),
ja: i18n.t('common.langJapanese', { lng: 'ja' }),
ko: i18n.t('common.langKorean', { lng: 'ko' }),
pl: i18n.t('common.langPolish', { lng: 'pl' }),
pt_BR: i18n.t('common.langBrPortuguese', { lng: 'pt_BR' }),
pt: i18n.t('common.langPortuguese', { lng: 'pt' }),
ru: i18n.t('common.langRussian', { lng: 'ru' }),
zh_CN: i18n.t('common.langSimplifiedChinese', { lng: 'zh_CN' }),
es: i18n.t('common.langSpanish', { lng: 'es' }),
uk: i18n.t('common.langUkranian', { lng: 'ua' }),
};

return languagesToRender;
};
export default function LanguagePicker() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const language = useAppSelector(languageSelector);

return (
<IAIPopover
triggerComponent={
<IAIIconButton
aria-label={t('common.languagePickerLabel')}
tooltip={t('common.languagePickerLabel')}
icon={<FaLanguage />}
size="sm"
<Menu closeOnSelect={false}>
<Tooltip label={t('common.languagePickerLabel')} hasArrow>
<MenuButton
as={IconButton}
icon={<IoLanguage />}
variant="link"
data-variant="link"
fontSize={26}
aria-label={t('common.languagePickerLabel')}
fontSize={22}
minWidth={8}
/>
}
>
<VStack>{renderLanguagePicker()}</VStack>
</IAIPopover>
</Tooltip>
<MenuList>
<MenuOptionGroup value={language}>
{map(LANGUAGES, (languageName, l: keyof typeof LANGUAGES) => (
<MenuItemOption
key={l}
value={l}
onClick={() => dispatch(languageChanged(l))}
>
{languageName}
</MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { VStack } from '@chakra-ui/react';
import {
IconButton,
Menu,
MenuButton,
MenuItemOption,
MenuList,
MenuOptionGroup,
Tooltip,
} from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import { setCurrentTheme } from 'features/ui/store/uiSlice';
import type { ReactNode } from 'react';
import i18n from 'i18n';
import { map } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaPalette } from 'react-icons/fa';
import { FaPalette } from 'react-icons/fa';

export const THEMES = {
dark: i18n.t('common.darkTheme'),
light: i18n.t('common.lightTheme'),
green: i18n.t('common.greenTheme'),
ocean: i18n.t('common.oceanTheme'),
};

export default function ThemeChanger() {
const { t } = useTranslation();
Expand All @@ -17,51 +30,31 @@ export default function ThemeChanger() {
(state: RootState) => state.ui.currentTheme
);

const THEMES = {
dark: t('common.darkTheme'),
light: t('common.lightTheme'),
green: t('common.greenTheme'),
ocean: t('common.oceanTheme'),
};

const handleChangeTheme = (theme: string) => {
dispatch(setCurrentTheme(theme));
};

const renderThemeOptions = () => {
const themesToRender: ReactNode[] = [];

Object.keys(THEMES).forEach((theme) => {
themesToRender.push(
<IAIButton
isChecked={currentTheme === theme}
leftIcon={currentTheme === theme ? <FaCheck /> : undefined}
size="sm"
onClick={() => handleChangeTheme(theme)}
key={theme}
>
{THEMES[theme as keyof typeof THEMES]}
</IAIButton>
);
});

return themesToRender;
};

return (
<IAIPopover
triggerComponent={
<IAIIconButton
aria-label={t('common.themeLabel')}
size="sm"
<Menu closeOnSelect={false}>
<Tooltip label={t('common.themeLabel')} hasArrow>
<MenuButton
as={IconButton}
icon={<FaPalette />}
variant="link"
data-variant="link"
aria-label={t('common.themeLabel')}
fontSize={20}
icon={<FaPalette />}
minWidth={8}
/>
}
>
<VStack align="stretch">{renderThemeOptions()}</VStack>
</IAIPopover>
</Tooltip>
<MenuList>
<MenuOptionGroup value={currentTheme}>
{map(THEMES, (themeName, themeKey: keyof typeof THEMES) => (
<MenuItemOption
key={themeKey}
value={themeKey}
onClick={() => dispatch(setCurrentTheme(themeKey))}
>
{themeName}
</MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { isEqual, reduce, pickBy } from 'lodash-es';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { reduce, pickBy } from 'lodash-es';

export const systemSelector = (state: RootState) => state.system;

Expand All @@ -22,11 +23,7 @@ export const activeModelSelector = createSelector(
);
return { ...model_list[activeModel], name: activeModel };
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
defaultSelectorOptions
);

export const diffusersModelsSelector = createSelector(
Expand All @@ -42,9 +39,11 @@ export const diffusersModelsSelector = createSelector(

return diffusersModels;
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
defaultSelectorOptions
);

export const languageSelector = createSelector(
systemSelector,
(system) => system.language,
defaultSelectorOptions
);
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { InvokeLogLevel } from 'app/logging/useLogger';
import { TFuncKey } from 'i18next';
import { t } from 'i18next';
import { userInvoked } from 'app/store/actions';
import { LANGUAGES } from '../components/LanguagePicker';

export type CancelStrategy = 'immediate' | 'scheduled';

Expand Down Expand Up @@ -91,6 +92,7 @@ export interface SystemState {
infillMethods: InfillMethod[];
isPersisted: boolean;
shouldAntialiasProgressImage: boolean;
language: keyof typeof LANGUAGES;
}

export const initialSystemState: SystemState = {
Expand Down Expand Up @@ -125,6 +127,7 @@ export const initialSystemState: SystemState = {
canceledSession: '',
infillMethods: ['tile', 'patchmatch'],
isPersisted: false,
language: 'en',
};

export const systemSlice = createSlice({
Expand Down Expand Up @@ -272,6 +275,9 @@ export const systemSlice = createSlice({
isPersistedChanged: (state, action: PayloadAction<boolean>) => {
state.isPersisted = action.payload;
},
languageChanged: (state, action: PayloadAction<keyof typeof LANGUAGES>) => {
state.language = action.payload;
},
},
extraReducers(builder) {
/**
Expand Down Expand Up @@ -481,6 +487,7 @@ export const {
shouldLogToConsoleChanged,
isPersistedChanged,
shouldAntialiasProgressImageChanged,
languageChanged,
} = systemSlice.actions;

export default systemSlice.reducer;
10 changes: 5 additions & 5 deletions invokeai/frontend/web/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ if (import.meta.env.MODE === 'package') {
} else {
i18n
.use(Backend)
.use(
new LanguageDetector(null, {
lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`,
})
)
// .use(
// new LanguageDetector(null, {
// lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`,
// })
// )
.use(initReactI18next)
.init({
fallbackLng: 'en',
Expand Down