[WebUI] Localization Support (#2050)

* Initial Localization Implementation

* Fix Initial Spinner

* Language Picker Dropdown

* RU Localization Update

Co-Authored-By: Artur <83028930+netsvetaev@users.noreply.github.com>

* Fixed localization breaking themes

* useUpdateTranslation Hook

To force trigger translations for data objects

* Localize Tab Data

* Localize Prompt Input & Current Image Buttons

* Localize Gallery & Bug FIxes

Fix a bug where the delete image from the context menu wasn't working. Removed tooltips that were broken as they don't work in context menu.

* Fix localization breaking in production

* Add Toast Localization Support

* Localize Unified Canvas

* Localize WIP Tabs

* Localize Hotkeys

* Localize Settings

* RU Localization Update

Co-Authored-By: Artur <83028930+netsvetaev@users.noreply.github.com>

* Add Support for Italian and Portuguese

* Localize Toasts

* Fix width of language picker items

* Localize Backend Messages

* Disable Debug Messages

* Add Support for French

* Fix missing localization for a string in the SettingsModal

* Disable French

* Styling updates to normalize text and accommodate other langs

* Add Portuguese Brazilian

* Fix Hotkey headers not being localized.

* Fix styling issue on models tag in Settings

* Fix Slider Styling to accommodate different languages

* Fix slider styling in light mode.

* Add German

* Add Italian

* Add Polish

* Update Italian

* Localized Frontend Build

* Updated RU Translations

* Fresh Build with updated RU changes

* Bug Fixes and Loc Updates

* Updated Frontend Build

* Fresh Build

Co-authored-by: Artur <83028930+netsvetaev@users.noreply.github.com>
This commit is contained in:
blessedcoolant
2022-12-25 07:23:21 +13:00
committed by GitHub
parent f961e865f5
commit 1d34405f4f
234 changed files with 7114 additions and 1246 deletions

View File

@@ -6,7 +6,6 @@
.input-label {
color: var(--text-color-secondary);
margin-right: 0;
}
.input-entry {

View File

@@ -14,7 +14,7 @@ export default function IAIInput(props: IAIInputProps) {
label,
styleClass,
isDisabled = false,
fontSize = '1rem',
fontSize = 'sm',
width,
isInvalid,
...rest
@@ -25,12 +25,14 @@ export default function IAIInput(props: IAIInputProps) {
className={`input ${styleClass}`}
isInvalid={isInvalid}
isDisabled={isDisabled}
flexGrow={1}
>
<FormLabel
fontSize={fontSize}
marginBottom={1}
fontWeight="bold"
alignItems="center"
whiteSpace="nowrap"
marginBottom={0}
marginRight={0}
className="input-label"
>
{label}

View File

@@ -5,10 +5,6 @@
.invokeai__number-input-form-label {
color: var(--text-color-secondary);
margin-right: 0;
font-size: 1rem;
margin-bottom: 0;
white-space: nowrap;
&[data-focus] + .invokeai__number-input-root {
outline: none;

View File

@@ -44,7 +44,7 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
const IAINumberInput = (props: Props) => {
const {
label,
labelFontSize = '1rem',
labelFontSize = 'sm',
styleClass,
isDisabled = false,
showStepper = true,
@@ -130,6 +130,10 @@ const IAINumberInput = (props: Props) => {
className="invokeai__number-input-form-label"
style={{ display: label ? 'block' : 'none' }}
fontSize={labelFontSize}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
{...formLabelProps}
>
{label}

View File

@@ -7,7 +7,6 @@
.invokeai__select-label {
color: var(--text-color-secondary);
margin-right: 0;
}
.invokeai__select-picker {

View File

@@ -28,7 +28,7 @@ const IAISelect = (props: IAISelectProps) => {
tooltip,
tooltipProps,
size = 'sm',
fontSize = 'md',
fontSize = 'sm',
styleClass,
...rest
} = props;
@@ -47,8 +47,9 @@ const IAISelect = (props: IAISelectProps) => {
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
fontWeight="bold"
marginRight={0}
marginBottom={0}
whiteSpace="nowrap"
>
{label}

View File

@@ -1,7 +1,6 @@
.invokeai__slider-component {
display: flex;
gap: 1rem;
align-items: center;
padding-bottom: 0.5rem;
border-radius: 0.5rem;
.invokeai__slider-component-label {
min-width: max-content;
@@ -26,7 +25,7 @@
.invokeai__slider-mark {
font-size: 0.75rem;
font-weight: bold;
color: var(--slider-color);
color: var(--slider-mark-color);
margin-top: 0.3rem;
}

View File

@@ -50,6 +50,7 @@ export type IAIFullSliderProps = {
isInputDisabled?: boolean;
tooltipSuffix?: string;
hideTooltip?: boolean;
isCompact?: boolean;
styleClass?: string;
sliderFormControlProps?: FormControlProps;
sliderFormLabelProps?: FormLabelProps;
@@ -83,6 +84,7 @@ export default function IAISlider(props: IAIFullSliderProps) {
inputReadOnly = true,
withReset = false,
hideTooltip = false,
isCompact = false,
handleReset,
isResetDisabled,
isSliderDisabled,
@@ -142,6 +144,18 @@ export default function IAISlider(props: IAIFullSliderProps) {
: `invokeai__slider-component`
}
data-markers={withSliderMarks}
style={
isCompact
? {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
columnGap: '1rem',
margin: 0,
padding: 0,
}
: {}
}
{...sliderFormControlProps}
>
<FormLabel
@@ -151,7 +165,7 @@ export default function IAISlider(props: IAIFullSliderProps) {
{label}
</FormLabel>
<HStack w={'100%'} gap={2}>
<HStack w={'100%'} gap={2} alignItems="center">
<Slider
aria-label={label}
value={value}

View File

@@ -1,33 +1,24 @@
.invokeai__switch-form-control {
.invokeai__switch-form-label {
display: flex;
column-gap: 1rem;
justify-content: space-between;
align-items: center;
color: var(--text-color-secondary);
font-size: 1rem;
margin-right: 0;
margin-bottom: 0.1rem;
white-space: nowrap;
width: auto;
}
.invokeai__switch-root {
.invokeai__switch-root {
span {
background-color: var(--switch-bg-color);
span {
background-color: var(--switch-bg-color);
background-color: var(--white);
}
}
&[data-checked] {
span {
background: var(--switch-bg-active-color);
span {
background-color: var(--white);
}
}
&[data-checked] {
span {
background: var(--switch-bg-active-color);
span {
background-color: var(--white);
}
}
}
}
}
}

View File

@@ -33,16 +33,26 @@ const IAISwitch = (props: Props) => {
isDisabled={isDisabled}
width={width}
className={`invokeai__switch-form-control ${styleClass}`}
display="flex"
columnGap="1rem"
alignItems="center"
justifyContent="space-between"
{...formControlProps}
>
<FormLabel
className="invokeai__switch-form-label"
whiteSpace="nowrap"
marginRight={0}
marginTop={0.5}
marginBottom={0.5}
fontSize="sm"
fontWeight="bold"
width="auto"
{...formLabelProps}
>
{label}
<Switch className="invokeai__switch-root" {...rest} />
</FormLabel>
<Switch className="invokeai__switch-root" {...rest} />
</FormControl>
);
};

View File

@@ -14,6 +14,7 @@ import { tabDict } from 'features/tabs/components/InvokeTabs';
import ImageUploadOverlay from './ImageUploadOverlay';
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
import useImageUploader from 'common/hooks/useImageUploader';
import { useTranslation } from 'react-i18next';
type ImageUploaderProps = {
children: ReactNode;
@@ -24,6 +25,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
const toast = useToast({});
const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const { setOpenUploader } = useImageUploader();
@@ -35,13 +37,13 @@ const ImageUploader = (props: ImageUploaderProps) => {
''
);
toast({
title: 'Upload failed',
title: t('toast:uploadFailed'),
description: msg,
status: 'error',
isClosable: true,
});
},
[toast]
[t, toast]
);
const fileAcceptedCallback = useCallback(
@@ -103,8 +105,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
if (imageItems.length > 1) {
toast({
description:
'Multiple images pasted, may only upload one image at a time',
description: t('toast:uploadFailedMultipleImagesDesc'),
status: 'error',
isClosable: true,
});
@@ -115,7 +116,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
if (!file) {
toast({
description: 'Unable to load file',
description: t('toast:uploadFailedUnableToLoadDesc'),
status: 'error',
isClosable: true,
});
@@ -128,7 +129,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
return () => {
document.removeEventListener('paste', pasteImageListener);
};
}, [dispatch, toast, activeTabName]);
}, [t, dispatch, toast, activeTabName]);
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
activeTabName

View File

@@ -1,13 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function NodesWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>Nodes</h1>
<p>
A node based system for the generation of images is under development
currently. Stay tuned for updates about this amazing feature.
</p>
<h1>{t('common:nodes')}</h1>
<p>{t('common:nodesDesc')}</p>
</div>
);
}

View File

@@ -1,24 +1,14 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export const PostProcessingWIP = () => {
const { t } = useTranslation();
return (
<div className="work-in-progress post-processing-work-in-progress">
<h1>Post Processing</h1>
<p>
Invoke AI offers a wide variety of post processing features. Image
Upscaling and Face Restoration are already available in the WebUI. You
can access them from the Advanced Options menu of the Text To Image and
Image To Image tabs. You can also process images directly, using the
image action buttons above the current image display or in the viewer.
</p>
<p>
A dedicated UI will be released soon to facilitate more advanced post
processing workflows.
</p>
<p>
The Invoke AI Command Line Interface offers various other features
including Embiggen.
</p>
<h1>{t('common:postProcessing')}</h1>
<p>{t('common:postProcessDesc1')}</p>
<p>{t('common:postProcessDesc2')}</p>
<p>{t('common:postProcessDesc3')}</p>
</div>
);
};

View File

@@ -1,15 +1,16 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function TrainingWIP() {
const { t } = useTranslation();
return (
<div className="work-in-progress nodes-work-in-progress">
<h1>Training</h1>
<h1>{t('common:training')}</h1>
<p>
A dedicated workflow for training your own embeddings and checkpoints
using Textual Inversion and Dreambooth from the web interface. <br />
{t('common:trainingDesc1')}
<br />
InvokeAI already supports training custom embeddings using Textual
Inversion using the main script.
<br />
{t('common:trainingDesc2')}
</p>
</div>
);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function useUpdateTranslations(fn: () => void) {
const { i18n } = useTranslation();
const currentLang = localStorage.getItem('i18nextLng');
React.useEffect(() => {
fn();
}, [fn]);
React.useEffect(() => {
i18n.on('languageChanged', () => {
fn();
});
}, [fn, i18n, currentLang]);
}