From 5bfaddd388f0e153f691740b1d913b3cb1665872 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Thu, 2 Mar 2023 08:42:40 -0600 Subject: [PATCH] feat(gui): add i18next and start localization --- gui/package.json | 3 +++ gui/src/components/ImageCard.tsx | 18 +++++++------ gui/src/components/ImageHistory.tsx | 5 +++- gui/src/components/LoadingCard.tsx | 11 +++++--- gui/src/main.tsx | 38 ++++++++++++++++++++++---- gui/src/strings/all.ts | 7 +++++ gui/src/strings/en.ts | 31 ++++++++++++++++++++++ gui/src/strings/fr.ts | 20 ++++++++++++++ gui/yarn.lock | 41 +++++++++++++++++++++++++++++ 9 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 gui/src/strings/all.ts create mode 100644 gui/src/strings/en.ts create mode 100644 gui/src/strings/fr.ts diff --git a/gui/package.json b/gui/package.json index eeddedb9..813b4723 100644 --- a/gui/package.json +++ b/gui/package.json @@ -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", diff --git a/gui/src/components/ImageCard.tsx b/gui/src/components/ImageCard.tsx index d252d14e..b8aca9fb 100644 --- a/gui/src/components/ImageCard.tsx +++ b/gui/src/components/ImageCard.tsx @@ -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'; @@ -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); @@ -110,7 +112,7 @@ export function ImageCard(props: ImageCardProps) { - + { const prevIndex = index - 1; if (prevIndex < 0) { @@ -127,7 +129,7 @@ export function ImageCard(props: ImageCardProps) { {visibleIndex(index)} of {outputs.length} - + { setIndex((index + 1) % outputs.length); }}> @@ -145,35 +147,35 @@ export function ImageCard(props: ImageCardProps) { {params.prompt} - + - + - + - + - + { setAnchor(event.currentTarget); }}> @@ -194,7 +196,7 @@ export function ImageCard(props: ImageCardProps) { - + diff --git a/gui/src/components/ImageHistory.tsx b/gui/src/components/ImageHistory.tsx index 0c3c30d3..713d3509 100644 --- a/gui/src/components/ImageHistory.tsx +++ b/gui/src/components/ImageHistory.tsx @@ -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'; @@ -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) { @@ -25,7 +28,7 @@ export function ImageHistory() { children.push(...history.map((item) => )); } else { if (doesExist(loading) === false) { - children.push(No results. Press Generate.); + children.push({t('history.empty')}); } } diff --git a/gui/src/components/LoadingCard.tsx b/gui/src/components/LoadingCard.tsx index ffcdec89..727fb0f9 100644 --- a/gui/src/components/LoadingCard.tsx +++ b/gui/src/components/LoadingCard.tsx @@ -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'; @@ -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), { @@ -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); @@ -112,8 +114,11 @@ export function LoadingCard(props: LoadingCardProps) { sx={{ alignItems: 'center' }} > {renderProgress()} - {getProgress()} of {getTotal()} steps - + {t('loading.progress', { + current: getProgress(), + total: getTotal(), + })} + diff --git a/gui/src/main.tsx b/gui/src/main.tsx index 91b23193..351d5fa2 100644 --- a/gui/src/main.tsx +++ b/gui/src/main.tsx @@ -1,12 +1,15 @@ /* 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'; @@ -14,7 +17,17 @@ 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; @@ -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, @@ -106,9 +132,11 @@ export async function main() { - - - + + + + + diff --git a/gui/src/strings/all.ts b/gui/src/strings/all.ts new file mode 100644 index 00000000..d75a7505 --- /dev/null +++ b/gui/src/strings/all.ts @@ -0,0 +1,7 @@ +import { I18N_STRINGS_EN, RequiredStrings } from './en.js'; +import { I18N_STRINGS_FR } from './fr.js'; + +export const I18N_STRINGS: Record = { + ...I18N_STRINGS_EN, + ...I18N_STRINGS_FR, +}; diff --git a/gui/src/strings/en.ts b/gui/src/strings/en.ts new file mode 100644 index 00000000..86abd2ea --- /dev/null +++ b/gui/src/strings/en.ts @@ -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']; diff --git a/gui/src/strings/fr.ts b/gui/src/strings/fr.ts new file mode 100644 index 00000000..722b4abf --- /dev/null +++ b/gui/src/strings/fr.ts @@ -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', + }, + } + }, +}; diff --git a/gui/yarn.lock b/gui/yarn.lock index ebd8ec9c..f663735b 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -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" @@ -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" @@ -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" @@ -3095,6 +3131,11 @@ v8-to-istanbul@^9.0.0: "@types/istanbul-lib-coverage" "^2.0.1" 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: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"