mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Feat/ui/improve-language (#3399)
This commit is contained in:
commit
4270e7ae25
@ -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",
|
||||||
|
@ -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));
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user