From 4d6eea7e81aafb5fcd7acd67951f233b884768f2 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 12 May 2023 19:35:03 +1000
Subject: [PATCH 1/5] feat(ui): store language in redux
---
.../frontend/web/src/app/components/App.tsx | 11 +++-
.../system/components/LanguagePicker.tsx | 60 ++++++++++---------
.../features/system/store/systemSelectors.ts | 21 ++++---
.../src/features/system/store/systemSlice.ts | 7 +++
invokeai/frontend/web/src/i18n.ts | 10 ++--
5 files changed, 64 insertions(+), 45 deletions(-)
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index b920698f14c..3fbcbc49ea4 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -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';
@@ -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 = {};
@@ -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;
@@ -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));
diff --git a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
index c69d4f132b0..d1608972f0c 100644
--- a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
+++ b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
@@ -6,46 +6,50 @@ import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaLanguage } from 'react-icons/fa';
+import i18n from 'i18n';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { languageSelector } from '../store/systemSelectors';
+import { languageChanged } from '../store/systemSlice';
+
+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' }),
+};
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' }),
- };
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const language = useAppSelector(languageSelector);
const renderLanguagePicker = () => {
const languagesToRender: ReactNode[] = [];
Object.keys(LANGUAGES).forEach((lang) => {
+ const l = lang as keyof typeof LANGUAGES;
languagesToRender.push(
- ) : undefined
- }
- onClick={() => i18n.changeLanguage(lang)}
- aria-label={LANGUAGES[lang as keyof typeof LANGUAGES]}
+ isChecked={language === l}
+ leftIcon={language === l ? : undefined}
+ onClick={() => dispatch(languageChanged(l))}
+ aria-label={LANGUAGES[l]}
size="sm"
minWidth="200px"
>
- {LANGUAGES[lang as keyof typeof LANGUAGES]}
+ {LANGUAGES[l]}
);
});
diff --git a/invokeai/frontend/web/src/features/system/store/systemSelectors.ts b/invokeai/frontend/web/src/features/system/store/systemSelectors.ts
index 68265aa2dc9..d9fd836ece6 100644
--- a/invokeai/frontend/web/src/features/system/store/systemSelectors.ts
+++ b/invokeai/frontend/web/src/features/system/store/systemSelectors.ts
@@ -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;
@@ -22,11 +23,7 @@ export const activeModelSelector = createSelector(
);
return { ...model_list[activeModel], name: activeModel };
},
- {
- memoizeOptions: {
- resultEqualityCheck: isEqual,
- },
- }
+ defaultSelectorOptions
);
export const diffusersModelsSelector = createSelector(
@@ -42,9 +39,11 @@ export const diffusersModelsSelector = createSelector(
return diffusersModels;
},
- {
- memoizeOptions: {
- resultEqualityCheck: isEqual,
- },
- }
+ defaultSelectorOptions
+);
+
+export const languageSelector = createSelector(
+ systemSelector,
+ (system) => system.language,
+ defaultSelectorOptions
);
diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
index e9cbd21a15a..5cc6ca3a432 100644
--- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
@@ -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';
@@ -91,6 +92,7 @@ export interface SystemState {
infillMethods: InfillMethod[];
isPersisted: boolean;
shouldAntialiasProgressImage: boolean;
+ language: keyof typeof LANGUAGES;
}
export const initialSystemState: SystemState = {
@@ -125,6 +127,7 @@ export const initialSystemState: SystemState = {
canceledSession: '',
infillMethods: ['tile', 'patchmatch'],
isPersisted: false,
+ language: 'en',
};
export const systemSlice = createSlice({
@@ -272,6 +275,9 @@ export const systemSlice = createSlice({
isPersistedChanged: (state, action: PayloadAction) => {
state.isPersisted = action.payload;
},
+ languageChanged: (state, action: PayloadAction) => {
+ state.language = action.payload;
+ },
},
extraReducers(builder) {
/**
@@ -481,6 +487,7 @@ export const {
shouldLogToConsoleChanged,
isPersistedChanged,
shouldAntialiasProgressImageChanged,
+ languageChanged,
} = systemSlice.actions;
export default systemSlice.reducer;
diff --git a/invokeai/frontend/web/src/i18n.ts b/invokeai/frontend/web/src/i18n.ts
index 71d4dfb35fe..68b457eabe4 100644
--- a/invokeai/frontend/web/src/i18n.ts
+++ b/invokeai/frontend/web/src/i18n.ts
@@ -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',
From 7d582553f21b6b11d86148a778296467c4206e29 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 12 May 2023 19:50:34 +1000
Subject: [PATCH 2/5] feat(ui): use chakra menu for language picker
---
.../system/components/LanguagePicker.tsx | 70 ++++++++-----------
1 file changed, 30 insertions(+), 40 deletions(-)
diff --git a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
index d1608972f0c..e5544c9bb9e 100644
--- a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
+++ b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
@@ -1,15 +1,19 @@
-import type { ReactNode } from 'react';
-
-import { VStack } from '@chakra-ui/react';
-import IAIButton from 'common/components/IAIButton';
+import {
+ Menu,
+ MenuButton,
+ MenuItemOption,
+ MenuList,
+ MenuOptionGroup,
+ Tooltip,
+} from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
-import IAIPopover from 'common/components/IAIPopover';
import { useTranslation } from 'react-i18next';
-import { FaCheck, FaLanguage } from 'react-icons/fa';
+import { FaLanguage } from 'react-icons/fa';
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';
export const LANGUAGES = {
ar: i18n.t('common.langArabic', { lng: 'ar' }),
@@ -35,43 +39,29 @@ export default function LanguagePicker() {
const dispatch = useAppDispatch();
const language = useAppSelector(languageSelector);
- const renderLanguagePicker = () => {
- const languagesToRender: ReactNode[] = [];
- Object.keys(LANGUAGES).forEach((lang) => {
- const l = lang as keyof typeof LANGUAGES;
- languagesToRender.push(
- : undefined}
- onClick={() => dispatch(languageChanged(l))}
- aria-label={LANGUAGES[l]}
- size="sm"
- minWidth="200px"
- >
- {LANGUAGES[l]}
-
- );
- });
-
- return languagesToRender;
- };
-
return (
-
+
+
+
+ {map(LANGUAGES, (languageName, l: keyof typeof LANGUAGES) => (
+ dispatch(languageChanged(l))}
+ >
+ {languageName}
+
+ ))}
+
+
+
+
);
}
From eebaa50710bcd79496f06ea8eafae7408c8f3b9c Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 12 May 2023 19:52:21 +1000
Subject: [PATCH 3/5] fix(ui): fix language picker tooltip
---
invokeai/frontend/web/public/locales/en.json | 2 +-
.../system/components/LanguagePicker.tsx | 34 +++++++++----------
2 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index dccb77c2671..3592e141d02 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -25,7 +25,7 @@
"common": {
"hotkeysLabel": "Hotkeys",
"themeLabel": "Theme",
- "languagePickerLabel": "Language Picker",
+ "languagePickerLabel": "Language",
"reportBugLabel": "Report Bug",
"githubLabel": "Github",
"discordLabel": "Discord",
diff --git a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
index e5544c9bb9e..d34ee581edd 100644
--- a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
+++ b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
@@ -40,28 +40,28 @@ export default function LanguagePicker() {
const language = useAppSelector(languageSelector);
return (
-
-
+
+
+
+ {map(LANGUAGES, (languageName, l: keyof typeof LANGUAGES) => (
+ dispatch(languageChanged(l))}
+ >
+ {languageName}
+
+ ))}
+
+
+
);
}
From 78cf70eaad86e457de122d049cd604091fc15563 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 12 May 2023 20:04:10 +1000
Subject: [PATCH 4/5] fix(ui): tweak lang picker style
---
.../src/features/system/components/LanguagePicker.tsx | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
index d34ee581edd..3e4e423c3f5 100644
--- a/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
+++ b/invokeai/frontend/web/src/features/system/components/LanguagePicker.tsx
@@ -1,4 +1,5 @@
import {
+ IconButton,
Menu,
MenuButton,
MenuItemOption,
@@ -6,14 +7,13 @@ import {
MenuOptionGroup,
Tooltip,
} from '@chakra-ui/react';
-import IAIIconButton from 'common/components/IAIIconButton';
import { useTranslation } from 'react-i18next';
-import { FaLanguage } from 'react-icons/fa';
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';
export const LANGUAGES = {
ar: i18n.t('common.langArabic', { lng: 'ar' }),
@@ -43,10 +43,12 @@ export default function LanguagePicker() {
}
+ as={IconButton}
+ icon={}
variant="link"
aria-label={t('common.languagePickerLabel')}
+ fontSize={22}
+ minWidth={8}
/>
From 60a565d7deebf2ba1e056e1ed020e8919b8619e5 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 12 May 2023 20:04:29 +1000
Subject: [PATCH 5/5] feat(ui): use chakra menu for theme changer
---
.../system/components/ThemeChanger.tsx | 89 +++++++++----------
1 file changed, 41 insertions(+), 48 deletions(-)
diff --git a/invokeai/frontend/web/src/features/system/components/ThemeChanger.tsx b/invokeai/frontend/web/src/features/system/components/ThemeChanger.tsx
index ff825e9bf08..d9426eecf2c 100644
--- a/invokeai/frontend/web/src/features/system/components/ThemeChanger.tsx
+++ b/invokeai/frontend/web/src/features/system/components/ThemeChanger.tsx
@@ -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();
@@ -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(
- : undefined}
- size="sm"
- onClick={() => handleChangeTheme(theme)}
- key={theme}
- >
- {THEMES[theme as keyof typeof THEMES]}
-
- );
- });
-
- return themesToRender;
- };
-
return (
-
+
+ }
variant="link"
- data-variant="link"
+ aria-label={t('common.themeLabel')}
fontSize={20}
- icon={}
+ minWidth={8}
/>
- }
- >
- {renderThemeOptions()}
-
+
+
+
+ {map(THEMES, (themeName, themeKey: keyof typeof THEMES) => (
+ dispatch(setCurrentTheme(themeKey))}
+ >
+ {themeName}
+
+ ))}
+
+
+
);
}