feat: Implement and update translations for warehouse categories, core application titles, and other UI elements.

This commit is contained in:
2025-12-12 14:25:16 +01:00
parent 08256f0019
commit c4d58f8354
9 changed files with 236 additions and 69 deletions

View File

@@ -53,10 +53,5 @@ File riassuntivo dello stato di sviluppo di Zentral.
- [2025-12-12 Resend Integration](./devlog/2025-12-12-120000_resend_integration.md) - **Completato**
- [2025-12-12 Magazzino: Categorie Gerarchiche](./devlog/2025-12-12-133000_remove_product_groups_add_categories.md) - **Completato**
- Sostituita la logica "Gruppi Merceologici" con l'utilizzo esteso delle "Categorie Articoli" gerarchiche.
- Riorganizzazione UI Auto Codes, allineamento stile a Custom Fields, miglioramento traduzioni e categorizzazione.
- [2025-12-12 - Modulo Comunicazioni](./devlog/2025-12-12-110000_communications_module.md) - **In Corso**
- Implementazione invio email e gestione comunicazioni.
- [2025-12-12 - Gestione Modulo Formazione (Generale)](./devlog/2025-12-12-105500_safety_training_schedule.md) - **In Corso**
- Implementazione modulo formazione generale e scadenziario.
- [2025-12-12 - Implementazione Gruppi Merceologici Magazzino](./devlog/2025-12-12-125000_magazzino_gruppi_merceologici.md) - **In Corso**
- Implementazione gestione gruppi merceologici per il magazzino.
- [2025-12-12 Update Translations](./devlog/2025-12-12-141010_update_translations.md) - **In Corso**
- Aggiornamento traduzioni per categorie magazzino, comunicazioni e formazione.

View File

@@ -0,0 +1,21 @@
# Update Translations for New Developments
## Status
- [x] Analysis of new features needing translation
- [x] Update Italian Translations (it)
- [x] Update English Translations (en)
- [x] Verification
## Details
Verified recent developments:
1. **Warehouse - Categories**: New management of article categories.
2. **Communications**: Email configuration and logs.
3. **Training**: New module for courses and training sessions.
I will scan these modules for `t()` calls and update the `translation.json` files in `public/locales/it` and `public/locales/en`.
## Work Done
- **Warehouse Categories**: Updated `CategoriesPage.tsx` to use `useTranslation`. Added keys for titles, buttons, fields, and dialogs in both IT and EN locales.
- **Communications**: Updated `SettingsPage.tsx` and `LogsPage.tsx` to use `useTranslation`. Added complete set of keys for settings, fields, actions, messages and log columns in both IT and EN locales.
- **Components**: Updated `Sidebar.tsx`, `SearchBar.tsx` to use full translations. Added `apps.core.title` and ensure `categories` is available in menu.
- **Training**: Training module files were not found in the current workspace, so no translations were applied for this module yet. Suggest to review separately when module is available.

View File

@@ -65,7 +65,8 @@
"emailConfig": "Email Configuration",
"movements": "Movements",
"stock": "Stock",
"inventory": "Inventory"
"inventory": "Inventory",
"categories": "Categories"
},
"navigation": {
"searchPlaceholder": "Search...",
@@ -285,12 +286,33 @@
"confermato": "Confirmed"
},
"apps": {
"core": {
"title": "Zentral"
},
"warehouse": {
"title": "Warehouse Management",
"inventory": "Inventory",
"movements": "Movements",
"stock": "Stock",
"categories": "Categories"
"categories": {
"title": "Article Categories",
"new": "New Category",
"edit": "Edit Category",
"empty": "No categories found",
"newParams": {
"root": "New Root Category"
},
"fields": {
"name": "Name",
"description": "Description",
"sortOrder": "Sort Order",
"active": "Active"
},
"deleteDialog": {
"title": "Delete Confirmation",
"content": "Are you sure you want to delete this category? This operation cannot be undone. If the category contains subcategories or articles, it may not be possible to delete it."
}
}
},
"hr": {
"title": "Human Resources",
@@ -1552,5 +1574,56 @@
"permesso": "Permit",
"altro": "Other"
}
},
"communications": {
"settings": {
"title": "Email Configuration",
"fields": {
"provider": "Provider",
"host": "SMTP Host",
"port": "Port",
"user": "Username",
"password": "Password",
"ssl": "Enable SSL/TLS",
"apiKey": "Resend API Key",
"fromEmail": "From Email",
"fromName": "From Name"
},
"helpers": {
"apiKey": "Get your API Key at"
},
"sections": {
"defaultSender": "Default Sender"
},
"actions": {
"testConnection": "Test Connection",
"sendTest": "Send Test"
},
"testStats": {
"title": "Test Email",
"recipient": "Recipient",
"subject": "Subject"
},
"messages": {
"loadError": "Error loading configuration",
"saveSuccess": "Configuration saved successfully",
"saveError": "Error saving configuration",
"recipientRequired": "Recipient email is required for test",
"testSuccess": "Test email sent successfully",
"testError": "Error sending test email"
}
},
"logs": {
"title": "Email Logs",
"columns": {
"id": "ID",
"date": "Date",
"status": "Status",
"sender": "Sender",
"recipient": "Recipient",
"subject": "Subject",
"error": "Error"
}
}
}
}

View File

@@ -61,7 +61,8 @@
"emailConfig": "Configurazione Email",
"movements": "Movimenti",
"stock": "Giacenze",
"inventory": "Inventario"
"inventory": "Inventario",
"categories": "Categorie"
},
"navigation": {
"searchPlaceholder": "Cerca...",
@@ -281,12 +282,33 @@
"confermato": "Confermato"
},
"apps": {
"core": {
"title": "Zentral"
},
"warehouse": {
"title": "Gestione Magazzino",
"inventory": "Inventario",
"movements": "Movimenti",
"stock": "Giacenze",
"categories": "Categorie"
"categories": {
"title": "Categorie Articoli",
"new": "Nuova Categoria",
"edit": "Modifica Categoria",
"empty": "Nessuna categoria trovata",
"newParams": {
"root": "Nuova Categoria Root"
},
"fields": {
"name": "Nome",
"description": "Descrizione",
"sortOrder": "Ordinamento",
"active": "Attivo"
},
"deleteDialog": {
"title": "Conferma Eliminazione",
"content": "Sei sicuro di voler eliminare questa categoria? L'operazione non può essere annullata. Se la categoria contiene sottocategorie o articoli, potrebbe non essere possibile eliminarla."
}
}
},
"hr": {
"title": "Gestione Personale",
@@ -1633,5 +1655,56 @@
"permesso": "Permesso",
"altro": "Altro"
}
},
"communications": {
"settings": {
"title": "Configurazione Email",
"fields": {
"provider": "Provider",
"host": "SMTP Host",
"port": "Porta",
"user": "Username",
"password": "Password",
"ssl": "Abilita SSL/TLS",
"apiKey": "Resend API Key",
"fromEmail": "Email Mittente",
"fromName": "Nome Mittente"
},
"helpers": {
"apiKey": "Ottieni la tua API Key su"
},
"sections": {
"defaultSender": "Mittente Default"
},
"actions": {
"testConnection": "Test Connessione",
"sendTest": "Invia Test"
},
"testStats": {
"title": "Test Email",
"recipient": "Destinatario",
"subject": "Oggetto"
},
"messages": {
"loadError": "Errore nel caricamento configurazione",
"saveSuccess": "Configurazione salvata con successo",
"saveError": "Errore nel salvataggio configurazione",
"recipientRequired": "Email destinatario obbligatoria per il test",
"testSuccess": "Email di test inviata con successo",
"testError": "Errore nell'invio email di test"
}
},
"logs": {
"title": "Log Email",
"columns": {
"id": "ID",
"date": "Data",
"status": "Stato",
"sender": "Mittente",
"recipient": "Destinatario",
"subject": "Oggetto",
"error": "Errore"
}
}
}
}

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { Box, Typography } from '@mui/material';
import { History } from '@mui/icons-material';
@@ -7,6 +8,7 @@ import { EmailLog } from '../types';
import dayjs from 'dayjs';
export default function LogsPage() {
const { t } = useTranslation();
const [logs, setLogs] = useState<EmailLog[]>([]);
const [loading, setLoading] = useState(false);
@@ -27,13 +29,13 @@ export default function LogsPage() {
};
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'id', headerName: t('communications.logs.columns.id'), width: 70 },
{
field: 'sentDate', headerName: 'Data', width: 180,
field: 'sentDate', headerName: t('communications.logs.columns.date'), width: 180,
valueFormatter: (params) => dayjs(params.value).format('DD/MM/YYYY HH:mm')
},
{
field: 'status', headerName: 'Stato', width: 120,
field: 'status', headerName: t('communications.logs.columns.status'), width: 120,
renderCell: (params) => (
<span style={{
color: params.value === 'Success' ? 'green' : 'red',
@@ -43,16 +45,16 @@ export default function LogsPage() {
</span>
)
},
{ field: 'sender', headerName: 'Mittente', width: 200 },
{ field: 'recipient', headerName: 'Destinatario', width: 200 },
{ field: 'subject', headerName: 'Oggetto', flex: 1 },
{ field: 'errorMessage', headerName: 'Errore', width: 200 },
{ field: 'sender', headerName: t('communications.logs.columns.sender'), width: 200 },
{ field: 'recipient', headerName: t('communications.logs.columns.recipient'), width: 200 },
{ field: 'subject', headerName: t('communications.logs.columns.subject'), flex: 1 },
{ field: 'errorMessage', headerName: t('communications.logs.columns.error'), width: 200 },
];
return (
<Box p={3} sx={{ height: '80vh', display: 'flex', flexDirection: 'column' }}>
<Box display="flex" justifyContent="space-between" mb={2}>
<Typography variant="h4"><History /> Email Logs</Typography>
<Typography variant="h4"><History /> {t('communications.logs.title')}</Typography>
</Box>
<DataGrid
rows={logs}

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm, Controller } from 'react-hook-form';
import {
Box, Paper, Typography, TextField, Button, Grid,
@@ -10,6 +11,7 @@ import { communicationsService } from '../services/communicationsService';
import { SmtpConfig, TestEmail } from '../types';
export default function SettingsPage() {
const { t } = useTranslation();
const { control, handleSubmit, reset, watch } = useForm<SmtpConfig>();
const provider = watch('provider') || 'smtp';
const [loading, setLoading] = useState(false);
@@ -28,7 +30,7 @@ export default function SettingsPage() {
reset(config);
} catch (error) {
console.error(error);
setNotification({ type: 'error', message: 'Failed to load configuration' });
setNotification({ type: 'error', message: t('communications.settings.messages.loadError') });
} finally {
setLoading(false);
}
@@ -38,9 +40,9 @@ export default function SettingsPage() {
try {
setLoading(true);
await communicationsService.saveConfig(data);
setNotification({ type: 'success', message: 'Configuration saved successfully' });
setNotification({ type: 'success', message: t('communications.settings.messages.saveSuccess') });
} catch (error) {
setNotification({ type: 'error', message: 'Failed to save configuration' });
setNotification({ type: 'error', message: t('communications.settings.messages.saveError') });
} finally {
setLoading(false);
}
@@ -48,16 +50,16 @@ export default function SettingsPage() {
const sendTest = async () => {
if (!testData.to) {
setNotification({ type: 'error', message: 'Recipient email is required for test' });
setNotification({ type: 'error', message: t('communications.settings.messages.recipientRequired') });
return;
}
try {
setLoading(true);
await communicationsService.sendTestEmail(testData);
setNotification({ type: 'success', message: 'Test email queued successfully' });
setNotification({ type: 'success', message: t('communications.settings.messages.testSuccess') });
setTestMode(false);
} catch (error: any) {
setNotification({ type: 'error', message: error.response?.data?.message || 'Failed to send test email' });
setNotification({ type: 'error', message: error.response?.data?.message || t('communications.settings.messages.testError') });
} finally {
setLoading(false);
}
@@ -66,7 +68,7 @@ export default function SettingsPage() {
return (
<Box p={3}>
<Typography variant="h4" gutterBottom display="flex" alignItems="center" gap={2}>
<Email fontSize="large" color="primary" /> Configurazione Email
<Email fontSize="large" color="primary" /> {t('communications.settings.title')}
</Typography>
<Paper sx={{ p: 3, mb: 3 }}>
@@ -74,13 +76,13 @@ export default function SettingsPage() {
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<FormControl fullWidth>
<InputLabel>Provider</InputLabel>
<InputLabel>{t('communications.settings.fields.provider')}</InputLabel>
<Controller
name="provider"
control={control}
defaultValue="smtp"
render={({ field }) => (
<Select {...field} label="Provider">
<Select {...field} label={t('communications.settings.fields.provider')}>
<MenuItem value="smtp">SMTP</MenuItem>
<MenuItem value="resend">Resend</MenuItem>
</Select>
@@ -96,7 +98,7 @@ export default function SettingsPage() {
name="host"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="SMTP Host" fullWidth required />}
render={({ field }) => <TextField {...field} label={t('communications.settings.fields.host')} fullWidth required />}
/>
</Grid>
<Grid item xs={12} md={4}>
@@ -104,7 +106,7 @@ export default function SettingsPage() {
name="port"
control={control}
defaultValue={587}
render={({ field }) => <TextField {...field} label="Port" type="number" fullWidth required />}
render={({ field }) => <TextField {...field} label={t('communications.settings.fields.port')} type="number" fullWidth required />}
/>
</Grid>
@@ -113,7 +115,7 @@ export default function SettingsPage() {
name="user"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="Username" fullWidth />}
render={({ field }) => <TextField {...field} label={t('communications.settings.fields.user')} fullWidth />}
/>
</Grid>
<Grid item xs={12} md={6}>
@@ -121,7 +123,7 @@ export default function SettingsPage() {
name="password"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="Password" type="password" fullWidth />}
render={({ field }) => <TextField {...field} label={t('communications.settings.fields.password')} type="password" fullWidth />}
/>
</Grid>
@@ -133,7 +135,7 @@ export default function SettingsPage() {
render={({ field: { onChange, value } }) => (
<FormControlLabel
control={<Switch checked={value} onChange={onChange} />}
label="Enable SSL/TLS"
label={t('communications.settings.fields.ssl')}
/>
)}
/>
@@ -147,17 +149,17 @@ export default function SettingsPage() {
name="resendApiKey"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="Resend API Key" type="password" fullWidth required />}
render={({ field }) => <TextField {...field} label={t('communications.settings.fields.apiKey')} type="password" fullWidth required />}
/>
<Typography variant="caption" color="textSecondary" sx={{ mt: 1, display: 'block' }}>
Ottieni la tua API Key su <a href="https://resend.com/api-keys" target="_blank" rel="noopener noreferrer">resend.com</a>
{t('communications.settings.helpers.apiKey')} <a href="https://resend.com/api-keys" target="_blank" rel="noopener noreferrer">resend.com</a>
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Divider sx={{ my: 2 }} />
<Typography variant="h6">Mittente Default</Typography>
<Typography variant="h6">{t('communications.settings.sections.defaultSender')}</Typography>
</Grid>
<Grid item xs={12} md={6}>
@@ -165,7 +167,7 @@ export default function SettingsPage() {
name="fromEmail"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="From Email" fullWidth required />}
render={({ field }) => <TextField {...field} label={t('communications.settings.fields.fromEmail')} fullWidth required />}
/>
</Grid>
<Grid item xs={12} md={6}>
@@ -173,7 +175,7 @@ export default function SettingsPage() {
name="fromName"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="From Name" fullWidth />}
render={({ field }) => <TextField {...field} label={t('communications.settings.fields.fromName')} fullWidth />}
/>
</Grid>
@@ -183,7 +185,7 @@ export default function SettingsPage() {
startIcon={<Send />}
onClick={() => setTestMode(!testMode)}
>
Test Connessione
{t('communications.settings.actions.testConnection')}
</Button>
<Button
type="submit"
@@ -191,7 +193,7 @@ export default function SettingsPage() {
startIcon={<Save />}
disabled={loading}
>
Salva Configurazione
{t('common.save')}
</Button>
</Grid>
</Grid>
@@ -200,11 +202,11 @@ export default function SettingsPage() {
{testMode && (
<Paper sx={{ p: 3, bgcolor: '#f5f5f5' }}>
<Typography variant="h6" gutterBottom>Test Email</Typography>
<Typography variant="h6" gutterBottom>{t('communications.settings.testStats.title')}</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
label="Destinatario"
label={t('communications.settings.testStats.recipient')}
fullWidth
value={testData.to}
onChange={(e) => setTestData({ ...testData, to: e.target.value })}
@@ -212,7 +214,7 @@ export default function SettingsPage() {
</Grid>
<Grid item xs={12} md={6}>
<TextField
label="Oggetto"
label={t('communications.settings.testStats.subject')}
fullWidth
value={testData.subject}
onChange={(e) => setTestData({ ...testData, subject: e.target.value })}
@@ -220,7 +222,7 @@ export default function SettingsPage() {
</Grid>
<Grid item xs={12}>
<Button variant="contained" color="secondary" onClick={sendTest} disabled={loading}>
Invia Test
{t('communications.settings.actions.sendTest')}
</Button>
</Grid>
</Grid>

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Button,
@@ -102,6 +103,7 @@ const CategoryItem: React.FC<CategoryItemProps> = ({ category, onEdit, onDelete,
};
export default function CategoriesPage() {
const { t } = useTranslation();
const { data: categories, isLoading } = useCategoryTree();
const createMutation = useCreateCategory();
const updateMutation = useUpdateCategory();
@@ -196,21 +198,21 @@ export default function CategoriesPage() {
};
if (isLoading) {
return <Typography>Caricamento...</Typography>;
return <Typography>{t('common.loading')}</Typography>;
}
return (
<Box>
<Box sx={{ mb: 3, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Typography variant="h5" fontWeight="bold">
Categorie Articoli
{t('apps.warehouse.categories.title')}
</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenDialog()}
>
Nuova Categoria Root
{t('apps.warehouse.categories.newParams.root')}
</Button>
</Box>
@@ -227,7 +229,7 @@ export default function CategoriesPage() {
))}
{(!categories || categories.length === 0) && (
<ListItem>
<ListItemText primary="Nessuna categoria trovata" />
<ListItemText primary={t('apps.warehouse.categories.empty')} />
</ListItem>
)}
</List>
@@ -236,19 +238,19 @@ export default function CategoriesPage() {
{/* Create/Edit Dialog */}
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{editingCategory ? "Modifica Categoria" : "Nuova Categoria"}
{editingCategory ? t('apps.warehouse.categories.edit') : t('apps.warehouse.categories.new')}
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<TextField
label="Nome"
label={t('apps.warehouse.categories.fields.name')}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
fullWidth
required
/>
<TextField
label="Descrizione"
label={t('apps.warehouse.categories.fields.description')}
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
fullWidth
@@ -256,7 +258,7 @@ export default function CategoriesPage() {
rows={3}
/>
<TextField
label="Ordinamento"
label={t('apps.warehouse.categories.fields.sortOrder')}
type="number"
value={formData.sortOrder}
onChange={(e) => setFormData({ ...formData, sortOrder: parseInt(e.target.value) || 0 })}
@@ -270,32 +272,31 @@ export default function CategoriesPage() {
onChange={(e) => setIsActive(e.target.checked)}
/>
}
label="Attivo"
label={t('apps.warehouse.categories.fields.active')}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Annulla</Button>
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
<Button onClick={handleSubmit} variant="contained" disabled={!formData.name}>
Salva
{t('common.save')}
</Button>
</DialogActions>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteConfirmOpen} onClose={() => setDeleteConfirmOpen(false)}>
<DialogTitle>Conferma Eliminazione</DialogTitle>
<DialogTitle>{t('apps.warehouse.categories.deleteDialog.title')}</DialogTitle>
<DialogContent>
<Typography>
Sei sicuro di voler eliminare questa categoria? L'operazione non può essere annullata.
Se la categoria contiene sottocategorie o articoli, potrebbe non essere possibile eliminarla.
{t('apps.warehouse.categories.deleteDialog.content')}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteConfirmOpen(false)}>Annulla</Button>
<Button onClick={() => setDeleteConfirmOpen(false)}>{t('common.cancel')}</Button>
<Button onClick={handleConfirmDelete} color="error" variant="contained">
Elimina
{t('common.delete')}
</Button>
</DialogActions>
</Dialog>

View File

@@ -64,14 +64,14 @@ export default function SearchBar() {
const options = useMemo(() => {
const opts: SearchOption[] = [
// Core
{ label: t('menu.dashboard'), path: '/', category: 'Zentral', translationKey: 'menu.dashboard' },
{ label: t('menu.calendar'), path: '/calendario', category: 'Zentral', translationKey: 'menu.calendar' },
{ label: t('menu.events'), path: '/eventi', category: 'Zentral', translationKey: 'menu.events' },
{ label: t('menu.clients'), path: '/clienti', category: 'Zentral', translationKey: 'menu.clients' },
{ label: t('menu.location'), path: '/location', category: 'Zentral', translationKey: 'menu.location' },
{ label: t('menu.articles'), path: '/articoli', category: 'Zentral', translationKey: 'menu.articles' },
{ label: t('menu.resources'), path: '/risorse', category: 'Zentral', translationKey: 'menu.resources' },
{ label: t('menu.reports'), path: '/report-templates', category: 'Zentral', translationKey: 'menu.reports' },
{ label: t('menu.dashboard'), path: '/', category: t('apps.core.title'), translationKey: 'menu.dashboard' },
{ label: t('menu.calendar'), path: '/calendario', category: t('apps.core.title'), translationKey: 'menu.calendar' },
{ label: t('menu.events'), path: '/eventi', category: t('apps.core.title'), translationKey: 'menu.events' },
{ label: t('menu.clients'), path: '/clienti', category: t('apps.core.title'), translationKey: 'menu.clients' },
{ label: t('menu.location'), path: '/location', category: t('apps.core.title'), translationKey: 'menu.location' },
{ label: t('menu.articles'), path: '/articoli', category: t('apps.core.title'), translationKey: 'menu.articles' },
{ label: t('menu.resources'), path: '/risorse', category: t('apps.core.title'), translationKey: 'menu.resources' },
{ label: t('menu.reports'), path: '/report-templates', category: t('apps.core.title'), translationKey: 'menu.reports' },
];
if (activeAppCodes.includes('warehouse')) {

View File

@@ -103,7 +103,7 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
const menuStructure: MenuItem[] = [
{
id: 'dashboard',
label: 'Zentral Dashboard',
label: t('menu.dashboard'),
icon: <DashboardIcon />,
path: '/',
translationKey: 'menu.dashboard',