Skip to content

Commit

Permalink
feat(gui): add i18next and start localization
Browse files Browse the repository at this point in the history
  • Loading branch information
ssube committed Mar 2, 2023
1 parent 9a0d205 commit 5bfaddd
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 17 deletions.
3 changes: 3 additions & 0 deletions gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"browser-bunyan": "^1.8.0",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"lodash": "^4.17.21",
"noicejs": "^5.0.0-3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.2.0",
"react-query": "^3.39.2",
"react-use": "^17.4.0",
"semver": "^7.3.8",
Expand Down
18 changes: 10 additions & 8 deletions gui/src/components/ImageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 * as React from 'react';
import { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHash } from 'react-use/lib/useHash';
import { useStore } from 'zustand';

Expand Down Expand Up @@ -96,6 +97,7 @@ export function ImageCard(props: ImageCardProps) {
}

const [index, setIndex] = useState(0);
const { t } = useTranslation();

const model = mustDefault(MODEL_LABELS[params.model], params.model);
const scheduler = mustDefault(SCHEDULER_LABELS[params.scheduler], params.scheduler);
Expand All @@ -110,7 +112,7 @@ export function ImageCard(props: ImageCardProps) {
<Box textAlign='center'>
<Grid container spacing={2}>
<GridItem xs={4}>
<Tooltip title='Previous'>
<Tooltip title={t('tooltip.previous')}>
<IconButton onClick={() => {
const prevIndex = index - 1;
if (prevIndex < 0) {
Expand All @@ -127,7 +129,7 @@ export function ImageCard(props: ImageCardProps) {
{visibleIndex(index)} of {outputs.length}
</GridItem>
<GridItem xs={4}>
<Tooltip title='Next'>
<Tooltip title={t('tooltip.next')}>
<IconButton onClick={() => {
setIndex((index + 1) % outputs.length);
}}>
Expand All @@ -145,35 +147,35 @@ export function ImageCard(props: ImageCardProps) {
<Box textAlign='left'>{params.prompt}</Box>
</GridItem>
<GridItem xs={2}>
<Tooltip title='Save'>
<Tooltip title={t('tooltip.save')}>
<IconButton onClick={downloadImage}>
<Download />
</IconButton>
</Tooltip>
</GridItem>
<GridItem xs={2}>
<Tooltip title='Img2img'>
<Tooltip title={t('tab.img2img')}>
<IconButton onClick={copySourceToImg2Img}>
<ContentCopy />
</IconButton>
</Tooltip>
</GridItem>
<GridItem xs={2}>
<Tooltip title='Inpaint'>
<Tooltip title={t('tab.inpaint')}>
<IconButton onClick={copySourceToInpaint}>
<Brush />
</IconButton>
</Tooltip>
</GridItem>
<GridItem xs={2}>
<Tooltip title='Upscale'>
<Tooltip title={t('tab.upscale')}>
<IconButton onClick={copySourceToUpscale}>
<ZoomOutMap />
</IconButton>
</Tooltip>
</GridItem>
<GridItem xs={2}>
<Tooltip title='Blend'>
<Tooltip title={t('tab.blend')}>
<IconButton onClick={(event) => {
setAnchor(event.currentTarget);
}}>
Expand All @@ -194,7 +196,7 @@ export function ImageCard(props: ImageCardProps) {
</Menu>
</GridItem>
<GridItem xs={2}>
<Tooltip title='Delete'>
<Tooltip title={t('tooltip.delete')}>
<IconButton onClick={deleteImage}>
<Delete />
</IconButton>
Expand Down
5 changes: 4 additions & 1 deletion gui/src/components/ImageHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { doesExist, mustExist } from '@apextoaster/js-utils';
import { Grid, Typography } from '@mui/material';
import { useContext } from 'react';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { useStore } from 'zustand';

import { StateContext } from '../state.js';
Expand All @@ -15,6 +16,8 @@ export function ImageHistory() {
// eslint-disable-next-line @typescript-eslint/unbound-method
const removeHistory = useStore(mustExist(useContext(StateContext)), (state) => state.removeHistory);

const { t } = useTranslation();

const children = [];

if (loading.length > 0) {
Expand All @@ -25,7 +28,7 @@ export function ImageHistory() {
children.push(...history.map((item) => <ImageCard key={`history-${item.outputs[0].key}`} value={item} onDelete={removeHistory} />));
} else {
if (doesExist(loading) === false) {
children.push(<Typography>No results. Press Generate.</Typography>);
children.push(<Typography>{t('history.empty')}</Typography>);
}
}

Expand Down
11 changes: 8 additions & 3 deletions gui/src/components/LoadingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Box, Button, Card, CardContent, CircularProgress, Typography } from '@m
import { Stack } from '@mui/system';
import * as React from 'react';
import { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { useStore } from 'zustand';

Expand Down Expand Up @@ -32,6 +33,7 @@ export function LoadingCard(props: LoadingCardProps) {
const pushHistory = useStore(state, (s) => s.pushHistory);
// eslint-disable-next-line @typescript-eslint/unbound-method
const setReady = useStore(state, (s) => s.setReady);
const { t } = useTranslation();

const cancel = useMutation(() => client.cancel(loading.outputs[index].key));
const ready = useQuery(`ready-${loading.outputs[index].key}`, () => client.ready(loading.outputs[index].key), {
Expand Down Expand Up @@ -63,7 +65,7 @@ export function LoadingCard(props: LoadingCardProps) {
const progress = getProgress();
if (progress > steps) {
// steps was not complete, show 99% until done
return 'many';
return t('loading.unknown');
}

return steps.toFixed(0);
Expand Down Expand Up @@ -112,8 +114,11 @@ export function LoadingCard(props: LoadingCardProps) {
sx={{ alignItems: 'center' }}
>
{renderProgress()}
<Typography>{getProgress()} of {getTotal()} steps</Typography>
<Button onClick={() => cancel.mutate()}>Cancel</Button>
<Typography>{t('loading.progress', {
current: getProgress(),
total: getTotal(),
})}</Typography>
<Button onClick={() => cancel.mutate()}>{t('loading.cancel')}</Button>
</Stack>
</Box>
</CardContent>
Expand Down
38 changes: 33 additions & 5 deletions gui/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
/* eslint-disable no-console */
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 { createRoot } from 'react-dom/client';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import { QueryClient, QueryClientProvider } from 'react-query';
import { satisfies } from 'semver';
import { createStore } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { createLogger } from 'browser-bunyan';

import { makeClient } from './client.js';
import { ParamsVersionError } from './components/error/ParamsVersion.js';
import { ServerParamsError } from './components/error/ServerParams.js';
import { OnnxError } from './components/OnnxError.js';
import { OnnxWeb } from './components/OnnxWeb.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;

Expand All @@ -37,6 +50,19 @@ export async function main() {
if (satisfies(version, PARAM_VERSION)) {
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
const {
createBrushSlice,
Expand Down Expand Up @@ -106,9 +132,11 @@ export async function main() {
<ClientContext.Provider value={client}>
<ConfigContext.Provider value={completeConfig}>
<LoggerContext.Provider value={logger}>
<StateContext.Provider value={state}>
<OnnxWeb />
</StateContext.Provider>
<I18nextProvider i18n={i18n}>
<StateContext.Provider value={state}>
<OnnxWeb />
</StateContext.Provider>
</I18nextProvider>
</LoggerContext.Provider>
</ConfigContext.Provider>
</ClientContext.Provider>
Expand Down
7 changes: 7 additions & 0 deletions gui/src/strings/all.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions gui/src/strings/en.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions gui/src/strings/fr.ts
Original file line number Diff line number Diff line change
@@ -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',
},
}
},
};
41 changes: 41 additions & 0 deletions gui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@
dependencies:
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":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f"
Expand Down Expand Up @@ -1852,11 +1859,32 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
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:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
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:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
Expand Down Expand Up @@ -2575,6 +2603,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.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:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
Expand Down Expand Up @@ -3095,6 +3131,11 @@ v8-to-istanbul@^9.0.0:
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^1.6.0"

[email protected]:
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:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
Expand Down

0 comments on commit 5bfaddd

Please sign in to comment.