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 ( - + + } - size="sm" variant="link" - data-variant="link" - fontSize={26} + aria-label={t('common.languagePickerLabel')} /> - } - > - {renderLanguagePicker()} - + + + {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 ( - - + + } variant="link" aria-label={t('common.languagePickerLabel')} /> - - - {map(LANGUAGES, (languageName, l: keyof typeof LANGUAGES) => ( - dispatch(languageChanged(l))} - > - {languageName} - - ))} - - - - + + + + {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} + + ))} + + + ); }