Feat/ui/improve-language (#3399)

This commit is contained in:
blessedcoolant 2023-05-12 23:32:50 +12:00 committed by GitHub
commit 4270e7ae25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 131 deletions

View File

@ -25,7 +25,7 @@
"common": { "common": {
"hotkeysLabel": "Hotkeys", "hotkeysLabel": "Hotkeys",
"themeLabel": "Theme", "themeLabel": "Theme",
"languagePickerLabel": "Language Picker", "languagePickerLabel": "Language",
"reportBugLabel": "Report Bug", "reportBugLabel": "Report Bug",
"githubLabel": "Github", "githubLabel": "Github",
"discordLabel": "Discord", "discordLabel": "Discord",

View File

@ -11,7 +11,7 @@ import { Box, Flex, Grid, Portal } from '@chakra-ui/react';
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel'; import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox'; 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 { memo, ReactNode, useCallback, useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import Loading from 'common/components/Loading/Loading'; 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 { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger'; import { useLogger } from 'app/logging/useLogger';
import ParametersDrawer from 'features/ui/components/ParametersDrawer'; import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -33,6 +35,9 @@ interface Props {
const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
useToastWatcher(); useToastWatcher();
useGlobalHotkeys(); useGlobalHotkeys();
const language = useAppSelector(languageSelector);
const log = useLogger(); const log = useLogger();
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
@ -43,6 +48,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
useEffect(() => { useEffect(() => {
log.info({ namespace: 'App', data: config }, 'Received config'); log.info({ namespace: 'App', data: config }, 'Received config');
dispatch(configChanged(config)); dispatch(configChanged(config));

View File

@ -1,73 +1,69 @@
import type { ReactNode } from 'react'; import {
IconButton,
import { VStack } from '@chakra-ui/react'; Menu,
import IAIButton from 'common/components/IAIButton'; MenuButton,
import IAIIconButton from 'common/components/IAIIconButton'; MenuItemOption,
import IAIPopover from 'common/components/IAIPopover'; MenuList,
MenuOptionGroup,
Tooltip,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; 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';
import { map } from 'lodash-es';
import { IoLanguage } from 'react-icons/io5';
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() { export default function LanguagePicker() {
const { t, i18n } = useTranslation(); const { t } = useTranslation();
const LANGUAGES = { const dispatch = useAppDispatch();
ar: t('common.langArabic', { lng: 'ar' }), const language = useAppSelector(languageSelector);
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 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>
);
});
return languagesToRender;
};
return ( return (
<IAIPopover <Menu closeOnSelect={false}>
triggerComponent={ <Tooltip label={t('common.languagePickerLabel')} hasArrow>
<IAIIconButton <MenuButton
aria-label={t('common.languagePickerLabel')} as={IconButton}
tooltip={t('common.languagePickerLabel')} icon={<IoLanguage />}
icon={<FaLanguage />}
size="sm"
variant="link" variant="link"
data-variant="link" aria-label={t('common.languagePickerLabel')}
fontSize={26} fontSize={22}
minWidth={8}
/> />
} </Tooltip>
<MenuList>
<MenuOptionGroup value={language}>
{map(LANGUAGES, (languageName, l: keyof typeof LANGUAGES) => (
<MenuItemOption
key={l}
value={l}
onClick={() => dispatch(languageChanged(l))}
> >
<VStack>{renderLanguagePicker()}</VStack> {languageName}
</IAIPopover> </MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
); );
} }

View File

@ -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 { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; 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 { 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 { 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() { export default function ThemeChanger() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -17,51 +30,31 @@ export default function ThemeChanger() {
(state: RootState) => state.ui.currentTheme (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 ( return (
<IAIPopover <Menu closeOnSelect={false}>
triggerComponent={ <Tooltip label={t('common.themeLabel')} hasArrow>
<IAIIconButton <MenuButton
aria-label={t('common.themeLabel')} as={IconButton}
size="sm"
variant="link"
data-variant="link"
fontSize={20}
icon={<FaPalette />} icon={<FaPalette />}
variant="link"
aria-label={t('common.themeLabel')}
fontSize={20}
minWidth={8}
/> />
} </Tooltip>
<MenuList>
<MenuOptionGroup value={currentTheme}>
{map(THEMES, (themeName, themeKey: keyof typeof THEMES) => (
<MenuItemOption
key={themeKey}
value={themeKey}
onClick={() => dispatch(setCurrentTheme(themeKey))}
> >
<VStack align="stretch">{renderThemeOptions()}</VStack> {themeName}
</IAIPopover> </MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
); );
} }

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store'; 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; export const systemSelector = (state: RootState) => state.system;
@ -22,11 +23,7 @@ export const activeModelSelector = createSelector(
); );
return { ...model_list[activeModel], name: activeModel }; return { ...model_list[activeModel], name: activeModel };
}, },
{ defaultSelectorOptions
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
); );
export const diffusersModelsSelector = createSelector( export const diffusersModelsSelector = createSelector(
@ -42,9 +39,11 @@ export const diffusersModelsSelector = createSelector(
return diffusersModels; return diffusersModels;
}, },
{ defaultSelectorOptions
memoizeOptions: { );
resultEqualityCheck: isEqual,
}, export const languageSelector = createSelector(
} systemSelector,
(system) => system.language,
defaultSelectorOptions
); );

View File

@ -24,6 +24,7 @@ import { InvokeLogLevel } from 'app/logging/useLogger';
import { TFuncKey } from 'i18next'; import { TFuncKey } from 'i18next';
import { t } from 'i18next'; import { t } from 'i18next';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { LANGUAGES } from '../components/LanguagePicker';
export type CancelStrategy = 'immediate' | 'scheduled'; export type CancelStrategy = 'immediate' | 'scheduled';
@ -91,6 +92,7 @@ export interface SystemState {
infillMethods: InfillMethod[]; infillMethods: InfillMethod[];
isPersisted: boolean; isPersisted: boolean;
shouldAntialiasProgressImage: boolean; shouldAntialiasProgressImage: boolean;
language: keyof typeof LANGUAGES;
} }
export const initialSystemState: SystemState = { export const initialSystemState: SystemState = {
@ -125,6 +127,7 @@ export const initialSystemState: SystemState = {
canceledSession: '', canceledSession: '',
infillMethods: ['tile', 'patchmatch'], infillMethods: ['tile', 'patchmatch'],
isPersisted: false, isPersisted: false,
language: 'en',
}; };
export const systemSlice = createSlice({ export const systemSlice = createSlice({
@ -272,6 +275,9 @@ export const systemSlice = createSlice({
isPersistedChanged: (state, action: PayloadAction<boolean>) => { isPersistedChanged: (state, action: PayloadAction<boolean>) => {
state.isPersisted = action.payload; state.isPersisted = action.payload;
}, },
languageChanged: (state, action: PayloadAction<keyof typeof LANGUAGES>) => {
state.language = action.payload;
},
}, },
extraReducers(builder) { extraReducers(builder) {
/** /**
@ -481,6 +487,7 @@ export const {
shouldLogToConsoleChanged, shouldLogToConsoleChanged,
isPersistedChanged, isPersistedChanged,
shouldAntialiasProgressImageChanged, shouldAntialiasProgressImageChanged,
languageChanged,
} = systemSlice.actions; } = systemSlice.actions;
export default systemSlice.reducer; export default systemSlice.reducer;

View File

@ -21,11 +21,11 @@ if (import.meta.env.MODE === 'package') {
} else { } else {
i18n i18n
.use(Backend) .use(Backend)
.use( // .use(
new LanguageDetector(null, { // new LanguageDetector(null, {
lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`, // lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`,
}) // })
) // )
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
fallbackLng: 'en', fallbackLng: 'en',