1
0
Fork 0

feat(gui): add i18next and start localization

This commit is contained in:
Sean Sube 2023-03-02 08:42:40 -06:00
parent 9a0d2051fb
commit 5bfaddd388
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
9 changed files with 157 additions and 17 deletions

View File

@ -16,10 +16,13 @@
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"browser-bunyan": "^1.8.0", "browser-bunyan": "^1.8.0",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"noicejs": "^5.0.0-3", "noicejs": "^5.0.0-3",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^12.2.0",
"react-query": "^3.39.2", "react-query": "^3.39.2",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"semver": "^7.3.8", "semver": "^7.3.8",

View File

@ -3,6 +3,7 @@ import { ArrowLeft, ArrowRight, Blender, Brush, ContentCopy, Delete, Download, Z
import { Box, Card, CardContent, CardMedia, Grid, IconButton, Menu, MenuItem, Paper, Tooltip } from '@mui/material'; import { Box, Card, CardContent, CardMedia, Grid, IconButton, Menu, MenuItem, Paper, Tooltip } from '@mui/material';
import * as React from 'react'; import * as React from 'react';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHash } from 'react-use/lib/useHash'; import { useHash } from 'react-use/lib/useHash';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
@ -96,6 +97,7 @@ export function ImageCard(props: ImageCardProps) {
} }
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const { t } = useTranslation();
const model = mustDefault(MODEL_LABELS[params.model], params.model); const model = mustDefault(MODEL_LABELS[params.model], params.model);
const scheduler = mustDefault(SCHEDULER_LABELS[params.scheduler], params.scheduler); const scheduler = mustDefault(SCHEDULER_LABELS[params.scheduler], params.scheduler);
@ -110,7 +112,7 @@ export function ImageCard(props: ImageCardProps) {
<Box textAlign='center'> <Box textAlign='center'>
<Grid container spacing={2}> <Grid container spacing={2}>
<GridItem xs={4}> <GridItem xs={4}>
<Tooltip title='Previous'> <Tooltip title={t('tooltip.previous')}>
<IconButton onClick={() => { <IconButton onClick={() => {
const prevIndex = index - 1; const prevIndex = index - 1;
if (prevIndex < 0) { if (prevIndex < 0) {
@ -127,7 +129,7 @@ export function ImageCard(props: ImageCardProps) {
{visibleIndex(index)} of {outputs.length} {visibleIndex(index)} of {outputs.length}
</GridItem> </GridItem>
<GridItem xs={4}> <GridItem xs={4}>
<Tooltip title='Next'> <Tooltip title={t('tooltip.next')}>
<IconButton onClick={() => { <IconButton onClick={() => {
setIndex((index + 1) % outputs.length); setIndex((index + 1) % outputs.length);
}}> }}>
@ -145,35 +147,35 @@ export function ImageCard(props: ImageCardProps) {
<Box textAlign='left'>{params.prompt}</Box> <Box textAlign='left'>{params.prompt}</Box>
</GridItem> </GridItem>
<GridItem xs={2}> <GridItem xs={2}>
<Tooltip title='Save'> <Tooltip title={t('tooltip.save')}>
<IconButton onClick={downloadImage}> <IconButton onClick={downloadImage}>
<Download /> <Download />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</GridItem> </GridItem>
<GridItem xs={2}> <GridItem xs={2}>
<Tooltip title='Img2img'> <Tooltip title={t('tab.img2img')}>
<IconButton onClick={copySourceToImg2Img}> <IconButton onClick={copySourceToImg2Img}>
<ContentCopy /> <ContentCopy />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</GridItem> </GridItem>
<GridItem xs={2}> <GridItem xs={2}>
<Tooltip title='Inpaint'> <Tooltip title={t('tab.inpaint')}>
<IconButton onClick={copySourceToInpaint}> <IconButton onClick={copySourceToInpaint}>
<Brush /> <Brush />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</GridItem> </GridItem>
<GridItem xs={2}> <GridItem xs={2}>
<Tooltip title='Upscale'> <Tooltip title={t('tab.upscale')}>
<IconButton onClick={copySourceToUpscale}> <IconButton onClick={copySourceToUpscale}>
<ZoomOutMap /> <ZoomOutMap />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</GridItem> </GridItem>
<GridItem xs={2}> <GridItem xs={2}>
<Tooltip title='Blend'> <Tooltip title={t('tab.blend')}>
<IconButton onClick={(event) => { <IconButton onClick={(event) => {
setAnchor(event.currentTarget); setAnchor(event.currentTarget);
}}> }}>
@ -194,7 +196,7 @@ export function ImageCard(props: ImageCardProps) {
</Menu> </Menu>
</GridItem> </GridItem>
<GridItem xs={2}> <GridItem xs={2}>
<Tooltip title='Delete'> <Tooltip title={t('tooltip.delete')}>
<IconButton onClick={deleteImage}> <IconButton onClick={deleteImage}>
<Delete /> <Delete />
</IconButton> </IconButton>

View File

@ -2,6 +2,7 @@ import { doesExist, mustExist } from '@apextoaster/js-utils';
import { Grid, Typography } from '@mui/material'; import { Grid, Typography } from '@mui/material';
import { useContext } from 'react'; import { useContext } from 'react';
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { StateContext } from '../state.js'; import { StateContext } from '../state.js';
@ -15,6 +16,8 @@ export function ImageHistory() {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const removeHistory = useStore(mustExist(useContext(StateContext)), (state) => state.removeHistory); const removeHistory = useStore(mustExist(useContext(StateContext)), (state) => state.removeHistory);
const { t } = useTranslation();
const children = []; const children = [];
if (loading.length > 0) { if (loading.length > 0) {
@ -25,7 +28,7 @@ export function ImageHistory() {
children.push(...history.map((item) => <ImageCard key={`history-${item.outputs[0].key}`} value={item} onDelete={removeHistory} />)); children.push(...history.map((item) => <ImageCard key={`history-${item.outputs[0].key}`} value={item} onDelete={removeHistory} />));
} else { } else {
if (doesExist(loading) === false) { if (doesExist(loading) === false) {
children.push(<Typography>No results. Press Generate.</Typography>); children.push(<Typography>{t('history.empty')}</Typography>);
} }
} }

View File

@ -3,6 +3,7 @@ import { Box, Button, Card, CardContent, CircularProgress, Typography } from '@m
import { Stack } from '@mui/system'; import { Stack } from '@mui/system';
import * as React from 'react'; import * as React from 'react';
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
@ -32,6 +33,7 @@ export function LoadingCard(props: LoadingCardProps) {
const pushHistory = useStore(state, (s) => s.pushHistory); const pushHistory = useStore(state, (s) => s.pushHistory);
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const setReady = useStore(state, (s) => s.setReady); const setReady = useStore(state, (s) => s.setReady);
const { t } = useTranslation();
const cancel = useMutation(() => client.cancel(loading.outputs[index].key)); const cancel = useMutation(() => client.cancel(loading.outputs[index].key));
const ready = useQuery(`ready-${loading.outputs[index].key}`, () => client.ready(loading.outputs[index].key), { const ready = useQuery(`ready-${loading.outputs[index].key}`, () => client.ready(loading.outputs[index].key), {
@ -63,7 +65,7 @@ export function LoadingCard(props: LoadingCardProps) {
const progress = getProgress(); const progress = getProgress();
if (progress > steps) { if (progress > steps) {
// steps was not complete, show 99% until done // steps was not complete, show 99% until done
return 'many'; return t('loading.unknown');
} }
return steps.toFixed(0); return steps.toFixed(0);
@ -112,8 +114,11 @@ export function LoadingCard(props: LoadingCardProps) {
sx={{ alignItems: 'center' }} sx={{ alignItems: 'center' }}
> >
{renderProgress()} {renderProgress()}
<Typography>{getProgress()} of {getTotal()} steps</Typography> <Typography>{t('loading.progress', {
<Button onClick={() => cancel.mutate()}>Cancel</Button> current: getProgress(),
total: getTotal(),
})}</Typography>
<Button onClick={() => cancel.mutate()}>{t('loading.cancel')}</Button>
</Stack> </Stack>
</Box> </Box>
</CardContent> </CardContent>

View File

@ -1,12 +1,15 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { mustDefault, mustExist, timeout } from '@apextoaster/js-utils'; import { mustDefault, mustExist, timeout } from '@apextoaster/js-utils';
import { createLogger } from 'browser-bunyan';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import * as React from 'react'; import * as React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { satisfies } from 'semver'; import { satisfies } from 'semver';
import { createStore } from 'zustand'; import { createStore } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware'; import { createJSONStorage, persist } from 'zustand/middleware';
import { createLogger } from 'browser-bunyan';
import { makeClient } from './client.js'; import { makeClient } from './client.js';
import { ParamsVersionError } from './components/error/ParamsVersion.js'; import { ParamsVersionError } from './components/error/ParamsVersion.js';
@ -14,7 +17,17 @@ import { ServerParamsError } from './components/error/ServerParams.js';
import { OnnxError } from './components/OnnxError.js'; import { OnnxError } from './components/OnnxError.js';
import { OnnxWeb } from './components/OnnxWeb.js'; import { OnnxWeb } from './components/OnnxWeb.js';
import { getApiRoot, loadConfig, mergeConfig, PARAM_VERSION } from './config.js'; import { getApiRoot, loadConfig, mergeConfig, PARAM_VERSION } from './config.js';
import { ClientContext, ConfigContext, createStateSlices, OnnxState, STATE_VERSION, StateContext, LoggerContext, STATE_KEY } from './state.js'; import {
ClientContext,
ConfigContext,
createStateSlices,
LoggerContext,
OnnxState,
STATE_KEY,
STATE_VERSION,
StateContext,
} from './state.js';
import { I18N_STRINGS } from './strings/all.js';
export const INITIAL_LOAD_TIMEOUT = 5_000; export const INITIAL_LOAD_TIMEOUT = 5_000;
@ -37,6 +50,19 @@ export async function main() {
if (satisfies(version, PARAM_VERSION)) { if (satisfies(version, PARAM_VERSION)) {
const completeConfig = mergeConfig(config, params); const completeConfig = mergeConfig(config, params);
// prep i18next
await i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
resources: I18N_STRINGS,
});
// prep zustand with a slice for each tab, using local storage // prep zustand with a slice for each tab, using local storage
const { const {
createBrushSlice, createBrushSlice,
@ -106,9 +132,11 @@ export async function main() {
<ClientContext.Provider value={client}> <ClientContext.Provider value={client}>
<ConfigContext.Provider value={completeConfig}> <ConfigContext.Provider value={completeConfig}>
<LoggerContext.Provider value={logger}> <LoggerContext.Provider value={logger}>
<I18nextProvider i18n={i18n}>
<StateContext.Provider value={state}> <StateContext.Provider value={state}>
<OnnxWeb /> <OnnxWeb />
</StateContext.Provider> </StateContext.Provider>
</I18nextProvider>
</LoggerContext.Provider> </LoggerContext.Provider>
</ConfigContext.Provider> </ConfigContext.Provider>
</ClientContext.Provider> </ClientContext.Provider>

7
gui/src/strings/all.ts Normal file
View File

@ -0,0 +1,7 @@
import { I18N_STRINGS_EN, RequiredStrings } from './en.js';
import { I18N_STRINGS_FR } from './fr.js';
export const I18N_STRINGS: Record<string, RequiredStrings> = {
...I18N_STRINGS_EN,
...I18N_STRINGS_FR,
};

31
gui/src/strings/en.ts Normal file
View File

@ -0,0 +1,31 @@
export const I18N_STRINGS_EN = {
en: {
translation: {
history: {
empty: 'No results. Press Generate to create an image.',
},
loading: {
cancel: 'Cancel',
progress: '{{current}} of {{total}} steps',
unknown: 'many',
},
tab: {
blend: 'Blend',
img2img: 'Img2img',
inpaint: 'Inpaint',
txt2txt: 'Txt2txt',
txt2img: 'Txt2img',
upscale: 'Upscale',
},
tooltip: {
delete: 'Delete',
next: 'EN Next',
previous: 'EN Previous',
save: 'Save',
},
}
},
};
// easy way to make sure all locales have the complete set of strings
export type RequiredStrings = typeof I18N_STRINGS_EN['en'];

20
gui/src/strings/fr.ts Normal file
View File

@ -0,0 +1,20 @@
export const I18N_STRINGS_FR = {
fr: {
translation: {
tab: {
blend: 'Blend',
img2img: 'Img2img',
inpaint: 'Inpaint',
txt2txt: 'Txt2txt',
txt2img: 'Txt2img',
upscale: 'Upscale',
},
tooltip: {
delete: 'Delete',
next: 'FR-Next',
previous: 'FR-Previous',
save: 'Save',
},
}
},
};

View File

@ -66,6 +66,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.11" regenerator-runtime "^0.13.11"
"@babel/runtime@^7.19.4":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/types@^7.18.6": "@babel/types@^7.18.6":
version "7.20.7" version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f"
@ -1852,11 +1859,32 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
hyphenate-style-name@^1.0.3: hyphenate-style-name@^1.0.3:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
i18next-browser-languagedetector@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz#ead34592edc96c6c3a618a51cb57ad027c5b5d87"
integrity sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==
dependencies:
"@babel/runtime" "^7.19.4"
i18next@^22.4.10:
version "22.4.10"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.4.10.tgz#cfbfc412c6bc83e3c16564f47e6a5c145255960e"
integrity sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==
dependencies:
"@babel/runtime" "^7.20.6"
ignore@^5.2.0: ignore@^5.2.0:
version "5.2.4" version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
@ -2575,6 +2603,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-i18next@^12.2.0:
version "12.2.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.2.0.tgz#010e3f6070b8d700442947233352ebe4b252d7a1"
integrity sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==
dependencies:
"@babel/runtime" "^7.20.6"
html-parse-stringify "^3.0.1"
react-is@^16.13.1, react-is@^16.7.0: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -3095,6 +3131,11 @@ v8-to-istanbul@^9.0.0:
"@types/istanbul-lib-coverage" "^2.0.1" "@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^1.6.0" convert-source-map "^1.6.0"
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
which-boxed-primitive@^1.0.2: which-boxed-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"