mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-10 02:14:59 -05:00
[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:
@@ -6,7 +6,6 @@
|
||||
|
||||
.input-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.input-entry {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
.invokeai__select-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.invokeai__select-picker {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
17
frontend/src/common/hooks/useUpdateTranslations.ts
Normal file
17
frontend/src/common/hooks/useUpdateTranslations.ts
Normal 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]);
|
||||
}
|
||||
Reference in New Issue
Block a user