Merge pull request #3 from sern-handler/localization

feat: localization support and spanish translation
This commit is contained in:
2023-08-16 19:58:37 +02:00
committed by GitHub
14 changed files with 268 additions and 68 deletions

View File

@@ -14,6 +14,7 @@ module.exports = {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: '.swcrc'
}, },
plugins: ['react-refresh'], plugins: ['react-refresh'],
rules: { rules: {
@@ -21,6 +22,6 @@ module.exports = {
'warn', 'warn',
{ allowConstantExport: true }, { allowConstantExport: true },
], ],
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off'
}, }
} }

View File

@@ -1,6 +1,6 @@
import path from 'node:path' import * as path from 'node:path'
import { app, BrowserWindow, dialog, ipcMain } from 'electron'; import { app, BrowserWindow, dialog, ipcMain } from 'electron';
import isDev from 'electron-is-dev'; import * as isDev from 'electron-is-dev';
import * as colorette from 'colorette'; import * as colorette from 'colorette';
import * as fs from 'node:fs' import * as fs from 'node:fs'
import * as os from 'node:os' import * as os from 'node:os'
@@ -47,7 +47,7 @@ function createWindow() {
}); });
}); });
ipcMain.on('submitForm', async (event, data) => { ipcMain.on('submitForm', async (event, data: InitModalData) => {
const fileName = createRandomFileName('txt') const fileName = createRandomFileName('txt')
// Process the submitted data here // Process the submitted data here
writeLineToLogAndConsole(`${colorette.green('✓')} Received sern init submit form data:`, fileName); writeLineToLogAndConsole(`${colorette.green('✓')} Received sern init submit form data:`, fileName);
@@ -192,4 +192,12 @@ function randomstring(length: number) {
counter += 1; counter += 1;
} }
return result; return result;
}
interface InitModalData {
projectName: string,
chosenTemplate: string,
installPackages: boolean,
chosenPackageManager: string,
selectedPath: string
} }

40
locales/en.json Normal file
View File

@@ -0,0 +1,40 @@
{
"credits": {
"propsTo": "@SrIzan10 (original language for this project)",
"btw": "All COMMENT_FOR_TRANSLATOR keys are comments that help you how to write translations. Follow these so you get approved faster."
},
"translation": {
"functionalityCard": {
"init": {
"description": "Scaffold a new project"
},
"plugins": {
"description": "Manage the plugins of an existing project"
}
},
"initModal": {
"openModalButton": "Get started",
"projectName": "Project name",
"selectTemplate": "Select a template",
"couldntFetchTemplates": "Couldn't fetch templates! Please do CTRL+R (u online?)",
"installPackagesCheckbox": "Install packages while you're at it",
"selectPackageManager": "Select a package manager",
"chooseDirectoryButton": "Select directory",
"selectedDirectory": "Selected directory:",
"goButton": "Go!",
"commandSuccessful": "The command was successful!",
"commandFailed": "The command failed!",
"COMMENT_FOR_TRANSLATOR": "keep this uppercase",
"openLogFile": "OPEN LOG FILE",
"COMMENT_FOR_TRANSLATOR_2": "the next string is used to change the text 'with' in the sentence 'with <package manager>'",
"with": "with"
},
"footer": {
"COMMENT_FOR_TRANSLATOR": "Keep all the strings here as lowercase. It kinda gives a good vibe to the text :D",
"web": "front page",
"COMMENT_FOR_TRANSLATOR_2": "Keep these at their current state. They are brands after all, so you shouldn't change these unless they are called differently in other languages",
"github": "github",
"discord": "discord"
}
}
}

40
locales/es.json Normal file
View File

@@ -0,0 +1,40 @@
{
"credits": {
"propsTo": "@SrIzan10 (spanish me)",
"btw": "Todos los COMMENT_FOR_TRANSLATOR son entradas que te ayudan a traducir los textos siguiendo unas reglas. Sigue estas para ser aprobado más fácilmente."
},
"translation": {
"functionalityCard": {
"init": {
"description": "Inicia un nuevo proyecto"
},
"plugins": {
"description": "Administra los plugins de un proyecto existente"
}
},
"initModal": {
"openModalButton": "Empezar",
"projectName": "Nombre del proyecto",
"selectTemplate": "Selecciona una plantilla",
"couldntFetchTemplatesError": "No se pudieron obtener las plantillas. Haz CTRL+R. (¿Estás conectado a internet?)",
"installPackagesCheckbox": "De paso instala las depencencias",
"selectPackageManager": "Selecciona un gestor de paquetes",
"chooseDirectoryButton": "Selecciona un directorio",
"selectedDirectory": "Directorio seleccionado:",
"goButton": "¡Vamos!",
"commandSuccessful": "El comando se ejecutó con éxito!",
"commandFailed": "El comando falló.",
"COMMENT_FOR_TRANSLATOR": "Mantén esto en mayúsculas",
"openLogFile": "Abrir archivo de registro",
"COMMENT_FOR_TRANSLATOR_2": "El texto de abajo sirve para reemplazar 'with' en las opciones de plantilla con lo de abajo. No cambiar.",
"with": "con"
},
"footer": {
"COMMENT_FOR_TRANSLATOR": "Mantén todos los textos aquí en minúsculas. Le dan un buen vibe al texto :D",
"web": "web",
"COMMENT_FOR_TRANSLATOR_2": "Mantén estos textos así. Al fin y al cabo son marcas, así que no deberías cambiarlas excepto si se llaman de otra forma en el idioma.",
"github": "github",
"discord": "discord"
}
}
}

View File

@@ -27,8 +27,10 @@
"@mui/material": "^5.13.4", "@mui/material": "^5.13.4",
"colorette": "^2.0.20", "colorette": "^2.0.20",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
"i18next": "^23.4.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"react-i18next": "^13.1.2"
}, },
"devDependencies": { "devDependencies": {
"@swc/cli": "^0.1.62", "@swc/cli": "^0.1.62",

View File

@@ -3,6 +3,7 @@ import Footer from './Footer.js';
import './FunctionalityCard.js'; import './FunctionalityCard.js';
import FunctionalityCard from './FunctionalityCard.js'; import FunctionalityCard from './FunctionalityCard.js';
import { ThemeProvider, createTheme } from '@mui/material/styles'; import { ThemeProvider, createTheme } from '@mui/material/styles';
import LanguageSelector from './LanguageSelector';
const darkTheme = createTheme({ const darkTheme = createTheme({
palette: { palette: {
@@ -17,15 +18,14 @@ function App() {
return ( return (
<div className="App"> <div className="App">
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>
<LanguageSelector />
<h1 className="titleHeader">~$ sern</h1> <h1 className="titleHeader">~$ sern</h1>
<div className="functionalityCards"> <div className="functionalityCards">
<FunctionalityCard <FunctionalityCard
command="init" command="init"
description="Scaffold a new project"
/> />
<FunctionalityCard <FunctionalityCard
command="plugins" command="plugins"
description="Install plugins on your existing project"
/> />
</div> </div>
<Footer /> <Footer />

View File

@@ -5,34 +5,38 @@ import GitHubIcon from '@mui/icons-material/GitHub';
import { faDiscord } from '@fortawesome/free-brands-svg-icons'; import { faDiscord } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import './Footer.css' import './Footer.css'
import { useTranslation } from 'react-i18next';
const { shell } = window.require('electron');
export default function Footer() { export default function Footer() {
const { t } = useTranslation('translation', { keyPrefix: 'footer' });
return ( return (
<div className="footer"> <div className="footer">
<Typography color="primary"> <Typography color="primary" sx={{ cursor: 'pointer' }}>
<Link href="https://sern.dev"> {/* this is such a hacky way to do this but it works(tm) */}
<PublicIcon color="primary" sx={{ fontSize: 'inherit', verticalAlign: 'middle', marginRight: '4px' }} /> <Link onClick={() => shell.openExternal('https://sern.dev')}>
<Typography variant="body1" component="span" sx={{ display: 'inline-block', verticalAlign: 'middle' }}> <PublicIcon color="primary" sx={{ fontSize: 'inherit', verticalAlign: 'middle', marginRight: '4px' }} />
front page <Typography variant="body1" component="span" sx={{ display: 'inline-block', verticalAlign: 'middle' }}>
</Typography> {t('web')}
</Typography>
</Link> </Link>
<span style={{ margin: '0 4px' }}></span> <span style={{ margin: '0 4px', cursor: 'default' }}></span>
<Link href="https://github.com/sern-handler"> <Link onClick={() => shell.openExternal('https://github.com/sern-handler')}>
<GitHubIcon color="primary" sx={{ fontSize: 'inherit', verticalAlign: 'middle', marginRight: '4px' }} /> <GitHubIcon color="primary" sx={{ fontSize: 'inherit', verticalAlign: 'middle', marginRight: '4px' }} />
<Typography variant="body1" component="span" sx={{ display: 'inline-block', verticalAlign: 'middle' }}> <Typography variant="body1" component="span" sx={{ display: 'inline-block', verticalAlign: 'middle' }}>
github github
</Typography> </Typography>
</Link> </Link>
<span style={{ margin: '0 4px' }}></span> <span style={{ margin: '0 4px', cursor: 'default' }}></span>
<Link href="https://sern.dev/discord"> <Link onClick={() => shell.openExternal('https://discord.gg/sern')}>
<FontAwesomeIcon icon={faDiscord} style={{ fontSize: 'inherit', verticalAlign: 'middle', marginRight: '4px' }} /> <FontAwesomeIcon icon={faDiscord} style={{ fontSize: 'inherit', verticalAlign: 'middle', marginRight: '4px' }} />
<Typography variant="body1" component="span" sx={{ display: 'inline-block', verticalAlign: 'middle' }}> <Typography variant="body1" component="span" sx={{ display: 'inline-block', verticalAlign: 'middle' }}>
discord discord
</Typography> </Typography>
</Link> </Link>
</Typography> </Typography>
</div> </div>

View File

@@ -4,39 +4,49 @@ import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import InitModal from './InitModal.js'; import InitModal from './InitModal.js';
import PluginsModal from './PluginsModal.js'; import PluginsModal from './PluginsModal.js';
import { useTranslation } from 'react-i18next';
function cardChooser(command: string) { function cardChooser(command: Props) {
switch (command) { const cmd = command.command
switch (cmd) {
case 'init': case 'init':
return <InitModal /> return <InitModal />
case 'plugins': case 'plugins':
return <PluginsModal /> return <PluginsModal />
default:
return null
} }
} }
export default function FunctionalityCard(props: Props) { export default function FunctionalityCard(props: Props) {
const { command, description } = props const { t } = useTranslation('translation', { keyPrefix: 'functionalityCard' });
const resolveDescription = (command: Props) => {
const cmd = command.command
switch (cmd) {
case 'init':
return t('init.description')
case 'plugins':
return t('plugins.description')
}
}
return ( return (
<Card sx={{ width: window.innerWidth / 2 }} variant='outlined'> <Card sx={{ width: window.innerWidth / 2 }} variant='outlined'>
<CardContent> <CardContent>
<Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom> <Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
{description} {resolveDescription(props)}
</Typography> </Typography>
<Typography color="text.primary"> <Typography color="text.primary">
<code>~$ sern {command}</code> <code>~$ sern {props.command}</code>
</Typography> </Typography>
</CardContent> </CardContent>
<CardActions> <CardActions>
{/*<Button size="small">Get started</Button>*/} {/*<Button size="small">Get started</Button>*/}
{cardChooser(command)} {cardChooser(props)}
</CardActions> </CardActions>
</Card> </Card>
); );
} }
interface Props { interface Props {
command: string command: 'init' | 'plugins'
description: string
} }

View File

@@ -17,22 +17,12 @@ import Alert from '@mui/material/Alert';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import './InitModal.css'; import './InitModal.css';
import { useTranslation } from 'react-i18next';
const { ipcRenderer } = window.require('electron'); const { ipcRenderer } = window.require('electron');
/* const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '2px solid #FFF',
boxShadow: 24,
padding: '20px',
color: 'white',
}; */
export default function InitModal() { export default function InitModal() {
const { t } = useTranslation('translation', { keyPrefix: 'initModal' });
const [loadingBecauseItsSettingUp, setLoadingBecauseItsSettingUp] = React.useState(false); const [loadingBecauseItsSettingUp, setLoadingBecauseItsSettingUp] = React.useState(false);
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -53,13 +43,13 @@ export default function InitModal() {
}; };
const [chosenPackageManager, setChosenPackageManager] = React.useState(''); const [chosenPackageManager, setChosenPackageManager] = React.useState('');
const handlePackageManagerChange = (event: SelectChangeEvent<string>) => { const handlePackageManagerChange = (event: SelectChangeEvent) => {
setChosenPackageManager(event.target.value); setChosenPackageManager(event.target.value);
}; };
const [templates, setTemplates] = React.useState<Array<TemplateList>>([]); const [templates, setTemplates] = React.useState<Array<TemplateList>>([]);
React.useEffect(() => { React.useEffect(() => {
fetch('https://raw.githubusercontent.com/sern-handler/create-bot/main/metadata/templateChoices.json') fetch('https://raw.githubusercontent.com/sern-handler/create-bot/main/metadata/templateChoices.jso')
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
setTemplates(data as TemplateList[]); setTemplates(data as TemplateList[]);
@@ -70,7 +60,7 @@ export default function InitModal() {
}, []); }, []);
if (templates.length === 0) { if (templates.length === 0) {
setTemplates([{ title: "Couldn't fetch templates! Please do CTRL+R", value: 'error' }]); setTemplates([{ title: t('couldntFetchTemplates'), value: 'error' }]);
} }
const [selectedPath, setSelectedPath] = React.useState(''); const [selectedPath, setSelectedPath] = React.useState('');
@@ -116,7 +106,7 @@ export default function InitModal() {
const snackbarAction = ( const snackbarAction = (
<React.Fragment> <React.Fragment>
<Button color="secondary" size="small" onClick={handleOpenLogFile}> <Button color="secondary" size="small" onClick={handleOpenLogFile}>
OPEN LOG FILE {t('openLogFile')}
</Button> </Button>
<IconButton <IconButton
size="small" size="small"
@@ -153,7 +143,7 @@ export default function InitModal() {
ipcRenderer.send('submitForm', data); ipcRenderer.send('submitForm', data);
ipcRenderer.on('submitForm', (_event, args) => { ipcRenderer.on('submitForm', (_event, args: IPCCommandExitEvent) => {
setLoading(false); setLoading(false);
setLoadingBecauseItsSettingUp(false); setLoadingBecauseItsSettingUp(false);
handleClose(); handleClose();
@@ -169,7 +159,7 @@ export default function InitModal() {
return ( return (
<div> <div>
<Button onClick={handleOpen}>Open modal</Button> <Button onClick={handleOpen}>{t('openModalButton')}</Button>
<Modal <Modal
open={open} open={open}
onClose={handleClose} onClose={handleClose}
@@ -183,7 +173,7 @@ export default function InitModal() {
<div className="formRow"> <div className="formRow">
<TextField <TextField
id="modal-form-projectName" id="modal-form-projectName"
label="Project name" label={t('projectName')}
variant="outlined" variant="outlined"
onChange={handleProjectNameChange} onChange={handleProjectNameChange}
required required
@@ -191,19 +181,19 @@ export default function InitModal() {
/> />
<FormControl fullWidth className="chooseTemplateForm"> <FormControl fullWidth className="chooseTemplateForm">
<InputLabel id="modal-form-templateLabel"> <InputLabel id="modal-form-templateLabel">
Select template {t('selectTemplate')}
</InputLabel> </InputLabel>
<Select <Select
labelId="modal-form-templateSelect" labelId="modal-form-templateSelect"
id="modal-form-templateSelect" id="modal-form-templateSelect"
value={chosenTemplate} value={chosenTemplate}
label="Select template" label={t('selectTemplate')}
onChange={handleTemplateChange} onChange={handleTemplateChange}
fullWidth fullWidth
> >
{templates.map((template) => ( {templates.map((template) => (
<MenuItem key={template.value} value={template.value}> <MenuItem key={template.value} value={template.value}>
{template.title} {template.title.replace('with', t('with'))}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
@@ -218,12 +208,12 @@ export default function InitModal() {
onChange={handlePackagesChange} onChange={handlePackagesChange}
/> />
} }
label="Install packages while you're at it" label={t('installPackagesCheckbox')}
/> />
</FormGroup> </FormGroup>
<FormControl className="choosePkgManagerForm" fullWidth> <FormControl className="choosePkgManagerForm" fullWidth>
<InputLabel id="modal-form-packageManagerLabel"> <InputLabel id="modal-form-packageManagerLabel">
Select package manager {t('selectPackageManager')}
</InputLabel> </InputLabel>
<Select <Select
labelId="modal-form-packageManagerLabel" labelId="modal-form-packageManagerLabel"
@@ -247,7 +237,7 @@ export default function InitModal() {
onClick={handleChooseDirButton} onClick={handleChooseDirButton}
sx={{ display: 'block', margin: '0 auto', marginTop: '5px' }} sx={{ display: 'block', margin: '0 auto', marginTop: '5px' }}
> >
Select directory {t('chooseDirectoryButton')}
</Button> </Button>
</div> </div>
<div className="formRow"> <div className="formRow">
@@ -256,7 +246,7 @@ export default function InitModal() {
component="div" component="div"
sx={{ display: 'block', margin: '0 auto', marginTop: '5px' }} sx={{ display: 'block', margin: '0 auto', marginTop: '5px' }}
> >
{selectedPath ? `Selected directory: ${selectedPath}` : ''} {selectedPath ? `${t('selectedDirectory')} ${selectedPath}` : ''}
</Typography> </Typography>
</div> </div>
<div className="bottomRight"> <div className="bottomRight">
@@ -266,19 +256,20 @@ export default function InitModal() {
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading || !isFormValid()} disabled={loading || !isFormValid()}
> >
{loading ? 'Go!' : 'Go!'} {/*{ loading ? 'Go!' : 'Go!' }*/}
{t('goButton')}
</Button> </Button>
</div> </div>
</Box> </Box>
</Modal> </Modal>
<Snackbar open={successSnackbarOpen} autoHideDuration={5000} onClose={handleSuccessSnackbarClose} action={snackbarAction}> <Snackbar open={successSnackbarOpen} autoHideDuration={5000} onClose={handleSuccessSnackbarClose} action={snackbarAction}>
<Alert onClose={handleSuccessSnackbarClose} severity="success" sx={{ width: '100%' }} action={snackbarAction}> <Alert onClose={handleSuccessSnackbarClose} severity="success" sx={{ width: '100%' }} action={snackbarAction}>
The command was successful! {t('commandSuccessful')}
</Alert> </Alert>
</Snackbar> </Snackbar>
<Snackbar open={errorSnackbarOpen} autoHideDuration={5000} onClose={handleErrorSnackbarClose} action={snackbarAction}> <Snackbar open={errorSnackbarOpen} autoHideDuration={5000} onClose={handleErrorSnackbarClose} action={snackbarAction}>
<Alert onClose={handleErrorSnackbarClose} severity="error" sx={{ width: '100%' }} action={snackbarAction}> <Alert onClose={handleErrorSnackbarClose} severity="error" sx={{ width: '100%' }} action={snackbarAction}>
The command was not successful {t('commandFailed')}
</Alert> </Alert>
</Snackbar> </Snackbar>
</div> </div>
@@ -288,4 +279,9 @@ export default function InitModal() {
interface TemplateList { interface TemplateList {
title: string title: string
value: string value: string
}
interface IPCCommandExitEvent {
exitCode: number | null
logFileName: string
} }

14
src/LanguageSelector.css Normal file
View File

@@ -0,0 +1,14 @@
.languageSelector {
position: absolute;
top: 15px;
right: 15px;
width: 70px;
}
.menuItems {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}

38
src/LanguageSelector.tsx Normal file
View File

@@ -0,0 +1,38 @@
import './LanguageSelector.css'
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import { useTranslation } from 'react-i18next';
export default function LanguageSelector() {
const { i18n } = useTranslation();
return (
<div className="languageSelector">
<FormControl fullWidth>
<InputLabel id="lang-select-label" />
<Select
labelId="lang-select-label"
id="lang-select"
defaultValue={i18n.language}
label=""
onChange={(event) => {
i18n.changeLanguage(event.target.value);
window.localStorage.setItem('lang', event.target.value);
}}
inputProps={{ IconComponent: () => null, sx: { padding: '0 !important' } }}
sx={{
height: '40px',
textAlign: 'center',
// this fixes a little text selection gap that appears in a
// few pixels outside the outer part of the selection "square"
cursor: 'pointer'
}}
>
<MenuItem value={'en'} className={'menuItems'}>🇺🇸</MenuItem>
<MenuItem value={'es'} className={'menuItems'}>🇪🇸</MenuItem>
</Select>
</FormControl>
</div>
);
}

View File

@@ -2,11 +2,30 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom/client'; import * as ReactDOM from 'react-dom/client';
import './main.css'; import './main.css';
import App from './App'; import App from './App';
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import enLocale from '../locales/en.json' assert { type: "json" };
import esLocale from '../locales/es.json' assert { type: "json" };
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources: {
en: enLocale,
es: esLocale
},
lng: window.localStorage.getItem('lang') || 'en',
fallbackLng: "en",
interpolation: {
escapeValue: false
}
});
const root = ReactDOM.createRoot(document.getElementById('root')!); const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>
); );

View File

@@ -18,7 +18,8 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true
}, },
"include": ["src", "src/vite-env.d.ts"], "include": ["src", "src/vite-env.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -2497,6 +2497,13 @@ hosted-git-info@^4.1.0:
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
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"
http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.1: http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
@@ -2539,6 +2546,13 @@ humanize-ms@^1.2.1:
dependencies: dependencies:
ms "^2.0.0" ms "^2.0.0"
i18next@^23.4.4:
version "23.4.4"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.4.4.tgz#ec8fb2b5f3c5d8e3bf3f8ab1b19e743be91300e0"
integrity sha512-+c9B0txp/x1m5zn+QlwHaCS9vyFtmIAEXbVSFzwCX7vupm5V7va8F9cJGNJZ46X9ZtoGzhIiRC7eTIIh93TxPA==
dependencies:
"@babel/runtime" "^7.22.5"
iconv-corefoundation@^1.1.7: iconv-corefoundation@^1.1.7:
version "1.1.7" version "1.1.7"
resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz#31065e6ab2c9272154c8b0821151e2c88f1b002a" resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz#31065e6ab2c9272154c8b0821151e2c88f1b002a"
@@ -3425,6 +3439,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@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-13.1.2.tgz#dbb1b18c364295af2a9072333ee4e0b43cbc2da8"
integrity sha512-D/OJ/8ZQYscabsvbCAiOgvJq8W3feQF/VIV0to1w7V7UvrUE1IZ3hcalOckUYvKBd7BP3b8EPm+hop3J8sS+Mw==
dependencies:
"@babel/runtime" "^7.22.5"
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"
@@ -4075,6 +4097,11 @@ vite@^4.4.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
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==
wcwidth@^1.0.1: wcwidth@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"