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',
sourceType: 'module',
tsconfigRootDir: __dirname,
project: '.swcrc'
},
plugins: ['react-refresh'],
rules: {
@@ -21,6 +22,6 @@ module.exports = {
'warn',
{ 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 isDev from 'electron-is-dev';
import * as isDev from 'electron-is-dev';
import * as colorette from 'colorette';
import * as fs from 'node:fs'
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')
// Process the submitted data here
writeLineToLogAndConsole(`${colorette.green('✓')} Received sern init submit form data:`, fileName);
@@ -192,4 +192,12 @@ function randomstring(length: number) {
counter += 1;
}
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",
"colorette": "^2.0.20",
"electron-is-dev": "^2.0.0",
"i18next": "^23.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-i18next": "^13.1.2"
},
"devDependencies": {
"@swc/cli": "^0.1.62",

View File

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

View File

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

View File

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

View File

@@ -17,22 +17,12 @@ import Alert from '@mui/material/Alert';
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
import './InitModal.css';
import { useTranslation } from 'react-i18next';
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() {
const { t } = useTranslation('translation', { keyPrefix: 'initModal' });
const [loadingBecauseItsSettingUp, setLoadingBecauseItsSettingUp] = React.useState(false);
const [open, setOpen] = React.useState(false);
@@ -53,13 +43,13 @@ export default function InitModal() {
};
const [chosenPackageManager, setChosenPackageManager] = React.useState('');
const handlePackageManagerChange = (event: SelectChangeEvent<string>) => {
const handlePackageManagerChange = (event: SelectChangeEvent) => {
setChosenPackageManager(event.target.value);
};
const [templates, setTemplates] = React.useState<Array<TemplateList>>([]);
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((data) => {
setTemplates(data as TemplateList[]);
@@ -70,7 +60,7 @@ export default function InitModal() {
}, []);
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('');
@@ -116,7 +106,7 @@ export default function InitModal() {
const snackbarAction = (
<React.Fragment>
<Button color="secondary" size="small" onClick={handleOpenLogFile}>
OPEN LOG FILE
{t('openLogFile')}
</Button>
<IconButton
size="small"
@@ -153,7 +143,7 @@ export default function InitModal() {
ipcRenderer.send('submitForm', data);
ipcRenderer.on('submitForm', (_event, args) => {
ipcRenderer.on('submitForm', (_event, args: IPCCommandExitEvent) => {
setLoading(false);
setLoadingBecauseItsSettingUp(false);
handleClose();
@@ -169,7 +159,7 @@ export default function InitModal() {
return (
<div>
<Button onClick={handleOpen}>Open modal</Button>
<Button onClick={handleOpen}>{t('openModalButton')}</Button>
<Modal
open={open}
onClose={handleClose}
@@ -183,7 +173,7 @@ export default function InitModal() {
<div className="formRow">
<TextField
id="modal-form-projectName"
label="Project name"
label={t('projectName')}
variant="outlined"
onChange={handleProjectNameChange}
required
@@ -191,19 +181,19 @@ export default function InitModal() {
/>
<FormControl fullWidth className="chooseTemplateForm">
<InputLabel id="modal-form-templateLabel">
Select template
{t('selectTemplate')}
</InputLabel>
<Select
labelId="modal-form-templateSelect"
id="modal-form-templateSelect"
value={chosenTemplate}
label="Select template"
label={t('selectTemplate')}
onChange={handleTemplateChange}
fullWidth
>
{templates.map((template) => (
<MenuItem key={template.value} value={template.value}>
{template.title}
{template.title.replace('with', t('with'))}
</MenuItem>
))}
</Select>
@@ -218,12 +208,12 @@ export default function InitModal() {
onChange={handlePackagesChange}
/>
}
label="Install packages while you're at it"
label={t('installPackagesCheckbox')}
/>
</FormGroup>
<FormControl className="choosePkgManagerForm" fullWidth>
<InputLabel id="modal-form-packageManagerLabel">
Select package manager
{t('selectPackageManager')}
</InputLabel>
<Select
labelId="modal-form-packageManagerLabel"
@@ -247,7 +237,7 @@ export default function InitModal() {
onClick={handleChooseDirButton}
sx={{ display: 'block', margin: '0 auto', marginTop: '5px' }}
>
Select directory
{t('chooseDirectoryButton')}
</Button>
</div>
<div className="formRow">
@@ -256,7 +246,7 @@ export default function InitModal() {
component="div"
sx={{ display: 'block', margin: '0 auto', marginTop: '5px' }}
>
{selectedPath ? `Selected directory: ${selectedPath}` : ''}
{selectedPath ? `${t('selectedDirectory')} ${selectedPath}` : ''}
</Typography>
</div>
<div className="bottomRight">
@@ -266,19 +256,20 @@ export default function InitModal() {
onClick={handleSubmit}
disabled={loading || !isFormValid()}
>
{loading ? 'Go!' : 'Go!'}
{/*{ loading ? 'Go!' : 'Go!' }*/}
{t('goButton')}
</Button>
</div>
</Box>
</Modal>
<Snackbar open={successSnackbarOpen} autoHideDuration={5000} onClose={handleSuccessSnackbarClose} action={snackbarAction}>
<Alert onClose={handleSuccessSnackbarClose} severity="success" sx={{ width: '100%' }} action={snackbarAction}>
The command was successful!
{t('commandSuccessful')}
</Alert>
</Snackbar>
<Snackbar open={errorSnackbarOpen} autoHideDuration={5000} onClose={handleErrorSnackbarClose} action={snackbarAction}>
<Alert onClose={handleErrorSnackbarClose} severity="error" sx={{ width: '100%' }} action={snackbarAction}>
The command was not successful
{t('commandFailed')}
</Alert>
</Snackbar>
</div>
@@ -288,4 +279,9 @@ export default function InitModal() {
interface TemplateList {
title: 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 './main.css';
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')!);
root.render(
<React.StrictMode>
<App />
<App />
</React.StrictMode>
);

View File

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

View File

@@ -2497,6 +2497,13 @@ hosted-git-info@^4.1.0:
dependencies:
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:
version "4.1.1"
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:
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:
version "1.1.7"
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"
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:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -4075,6 +4097,11 @@ vite@^4.4.0:
optionalDependencies:
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:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"