Le projet utilise un système de traduction basé sur TypeScript pour gérer le contenu multilingue. Les traductions sont stockées dans le dossier src/i18n/locales/
.
src/i18n/
├── locales/
│ ├── en/ # Dossier des traductions anglaises
│ │ ├── stats.ts
│ │ ├── filaments.ts
│ │ └── ...
│ ├── fr/ # Dossier des traductions françaises
│ │ ├── stats.ts
│ │ ├── filaments.ts
│ │ └── ...
│ ├── en.ts # Fichier principal des traductions anglaises
│ └── fr.ts # Fichier principal des traductions françaises
├── types.ts # Types pour les traductions
├── config.ts # Configuration et utilitaires de traduction
└── utils.ts # Utilitaires (getLanguageFromURL, etc.)
Les traductions sont organisées par modules fonctionnels. Par exemple, pour les statistiques :
// src/i18n/locales/fr/stats.ts
export const stats = {
title: "Statistiques",
description: "Statistiques des filaments",
// Statistiques générales
total_filaments: "Nombre total de filaments",
total_weight: "Poids total",
// Actions
actions: {
refresh: "Rafraîchir",
print: "Imprimer",
},
// Sous-sections
filaments_stats: {
title: "Filaments",
// ...
},
prints_stats: {
title: "Impressions",
// ...
},
};
-
Imports nécessaires :
--- import Layout from '@/layouts/Layout.astro'; import { getLangFromUrl, useTranslations } from '@/i18n/config'; ---
-
Récupération de la langue et utilisation des traductions :
--- const lang = getLangFromUrl(Astro.url); const t = useTranslations(lang); ---
-
Structure de la page :
<Layout title={t('stats.title')} description={t('stats.description')} lang={lang}> <div> <h1>{t('stats.title')}</h1> <p>{t('stats.description')}</p> </div> </Layout>
-
Import du hook :
import { useTranslations } from "@/i18n/config";
-
Utilisation dans le composant :
interface Props { lang: Language; } export default function MonComposant({ lang }: Props) { const t = useTranslations(lang); return ( <div> <h1>{t("stats.title")}</h1> <p>{t("stats.description")}</p> </div> ); }
-
Structure des fichiers de traduction :
- Chaque module (ex: stats, filaments) a son propre fichier dans les dossiers
en/
etfr/
- Les fichiers principaux
en.ts
etfr.ts
importent et agrègent tous les modules - Toujours ajouter les traductions dans les deux langues (FR et EN)
- Chaque module (ex: stats, filaments) a son propre fichier dans les dossiers
-
Exemple d'ajout d'une nouvelle section :
// Dans src/i18n/locales/fr/stats.ts export const stats = { // ... traductions existantes nouvelle_section: { title: "Titre de la section", description: "Description de la section", // ... autres traductions }, }; // Dans src/i18n/locales/en/stats.ts export const stats = { // ... existing translations nouvelle_section: { title: "Section title", description: "Section description", // ... other translations }, };
-
Bonnes pratiques :
- Utiliser des clés descriptives et cohérentes
- Organiser les traductions de manière hiérarchique
- Maintenir la même structure dans toutes les langues
- Vérifier que toutes les clés existent dans toutes les langues
- Utiliser des valeurs appropriées pour chaque langue
-
Accès simple :
t("stats.title");
-
Avec des variables :
`${value} ${t("stats.unit")}`;
-
Pour les actions :
const actions = { label: t("stats.actions.refresh"), onClickId: "refresh-stats", };
-
Vérification des clés manquantes :
- S'assurer que chaque clé existe dans toutes les langues
- Maintenir la même structure hiérarchique
-
Mise à jour des traductions :
- Modifier les fichiers de traduction correspondants
- Tester l'affichage dans toutes les langues
- Vérifier la cohérence des traductions
-
Tests :
- Vérifier l'affichage dans l'interface
- Tester le changement de langue
- Valider les traductions avec les utilisateurs
Chaque page principale suit une structure cohérente :
-
Bandeau d'en-tête (Header Banner)
- Dégradé de couleur personnalisable via les variables CSS
- Titre principal et sous-titre
- Actions principales
- Classes :
header-banner
,header-banner-content
,header-banner-title
-
Sections de contenu
- Cartes avec ombres et coins arrondis
- Icônes dans des cercles avec couleurs pastels
- Classes :
section-card
,section-card-title
-
Icônes
- Toujours affichées dans des cercles
- Tailles standardisées :
- Grande :
section-icon
(40x40px) - Petite :
section-icon-sm
(32x32px)
- Grande :
- Couleurs pastels prédéfinies :
section-icon-blue
section-icon-purple
section-icon-green
section-icon-yellow
section-icon-amber
-
Boutons
- Style principal :
header-banner-button
- Avec icône :
header-banner-button-icon
- Style principal :
Les couleurs du bandeau sont personnalisables via les variables CSS :
:root {
--banner-from-color: theme("colors.blue.500");
--banner-to-color: theme("colors.purple.600");
}
Pour le thème sombre :
[data-theme="dark"] {
--banner-from-color: theme("colors.blue.600");
--banner-to-color: theme("colors.purple.700");
}
Exemple de structure de page :
<div class="header-banner">
<div class="header-banner-content">
<h1 class="header-banner-title">Titre de la page</h1>
<p class="header-banner-subtitle">Description</p>
</div>
</div>
<div class="section-card">
<h2 class="section-card-title">
<div class="section-icon section-icon-blue">
<Icon />
</div>
Titre de section
</h2>
<!-- Contenu -->
</div>
-
Types de base :
- Les traductions retournent un type
TranslationValue
qui peut être soit unestring
soit une fonction retournant unestring
- Pour convertir une traduction en string, utilisez la fonction
getTranslationString()
:
import { getTranslationString } from "@/lib/utils/translation"; // Dans un composant const t = useTranslations(lang); const title = getTranslationString(t("page.title"));
- Les traductions retournent un type
-
Utilisation dans les composants :
- Pour les props qui attendent une string, utilisez toujours
getTranslationString()
:
<Button disabled={isLoading}> {getTranslationString(t('common.save'))} </Button>
- Pour les props qui attendent une string, utilisez toujours
-
Bonnes pratiques :
- Utilisez
getTranslationString()
pour les props qui attendent une string - Pour les textes affichés directement dans le JSX, vous pouvez utiliser directement
t()
- Vérifiez les types TypeScript pour éviter les erreurs de compilation
- Utilisez
Le système de gestion des stocks de filaments permet d'ajouter et de retirer des bobines entières de filament, en tenant compte du poids défini pour chaque type de filament.
-
Composants principaux :
-
SpoolManager
: Interface utilisateur pour gérer le stock -
TargetSpoolsManager
: Interface pour définir le stock cible -
FilamentHistory
: Affichage de l'historique des modifications
-
-
API :
-
/api/filaments/[id]/spool
: Endpoint pour ajouter/retirer du stock -
/api/filaments/[id]/target
: Endpoint pour définir le stock cible
-
Le composant SpoolManager
affiche l'état actuel du stock et permet d'ajouter ou de retirer des bobines complètes.
<SpoolManager
filamentId={filament.id}
currentStock={remainingWeight}
targetStock={targetTotalWeight}
_lang={lang}
client:load
/>
-
filamentId
: Identifiant unique du filament -
currentStock
: Poids actuel restant en grammes -
targetStock
: Poids cible total en grammes -
_lang
: Langue courante pour les traductions
-
Affichage :
- Poids en kilogrammes (pour plus de lisibilité)
- Nombre de bobines actuelles et cibles
- Pourcentage du stock par rapport à l'objectif
- Radar visuel montrant la proportion du stock
-
Actions :
- Bouton pour ajouter une bobine entière
- Bouton pour retirer une bobine entière
- Validations pour empêcher le stock négatif
L'API /api/filaments/[id]/spool
est conçue pour gérer les ajouts et retraits de stock.
// Exemple d'appel pour ajouter du stock
fetch(`/api/filaments/${filamentId}/spool`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ weight: 1000 }), // Poids positif pour ajouter
});
// Exemple d'appel pour retirer du stock
fetch(`/api/filaments/${filamentId}/spool`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ weight: -1000 }), // Poids négatif pour retirer
});
-
Récupération du contexte :
- Poids actuel du filament (
remaining_weight
) - Poids d'une bobine individuelle (
weight
)
- Poids actuel du filament (
-
Calcul du nouveau poids :
- Addition du poids fourni au poids actuel
- Validation que le résultat n'est pas négatif
-
Mise à jour de la base de données :
- Mise à jour du champ
remaining_weight
- Ajout d'une entrée dans l'historique avec l'opération et la quantité
- Mise à jour du champ
-
Réponse :
{ "success": true, "previousWeight": 1000, "newWeight": 2000, "spoolWeight": 1000, "difference": 1000, "totalSpools": 2 }
Chaque modification de stock est enregistrée dans la table filament_history
avec les informations suivantes :
-
filament_id
: ID du filament concerné -
operation
: Type d'opération ('add' ou 'remove') -
quantity
: Quantité ajoutée ou retirée (toujours positive) -
user_id
: ID de l'utilisateur ayant effectué l'opération -
created_at
: Date et heure de l'opération
-
Opérations par bobines entières :
- Toujours ajouter ou retirer des bobines entières
- Le poids d'une bobine est défini dans la base de données pour chaque filament
-
Validation des données :
- Vérifier que le stock ne devient pas négatif
- Valider les entrées utilisateur
- Utiliser des valeurs par défaut appropriées
-
Affichage des informations :
- Afficher les poids en kilogrammes (plus lisible)
- Afficher également le nombre de bobines
- Utiliser des indicateurs visuels (comme le radar de stock)
-
Gestion des erreurs :
- Afficher des messages d'erreur clairs
- Utiliser le composant Toast pour les notifications
- Logger les erreurs pour le débogage
// Composant SpoolManager (version simplifiée)
function SpoolManager({ filamentId, currentStock, targetStock, _lang }) {
const t = useTranslations(_lang);
const handleUpdateStock = async (action) => {
try {
const weightChange = action === "add" ? spoolWeight : -spoolWeight;
await fetch(`/api/filaments/${filamentId}/spool`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ weight: weightChange }),
});
// Notification et rafraîchissement
} catch (error) {
// Gestion des erreurs
}
};
return (
<div>
{/* Affichage du stock */}
<div>
{currentStockKg} kg ({currentSpools} bobines)
</div>
{/* Boutons d'action */}
<Button onClick={() => handleUpdateStock("add")}>
Ajouter une bobine
</Button>
<Button onClick={() => handleUpdateStock("remove")}>
Retirer une bobine
</Button>
</div>
);
}
Le système d'authentification utilise bcrypt pour le hachage sécurisé des mots de passe et des sessions basées sur des tokens.
Deux comptes de test sont disponibles pour le développement :
-
Compte Administrateur
- Email : bobdivx@gmail.com
- Mot de passe : 8tc6vr89
- Rôle : admin
-
Compte Utilisateur Test
- Email : test@exemple.com
- Mot de passe : password
- Rôle : user
-
Connexion
- Les identifiants sont envoyés à
/api/auth/login
- Le serveur vérifie l'existence de l'utilisateur
- Le mot de passe est vérifié avec bcrypt
- Une session est créée avec un token unique
- Un cookie sécurisé est défini avec le token
- Les identifiants sont envoyés à
-
Sécurité
- Les mots de passe sont hachés avec bcrypt (10 rounds)
- Les sessions expirent après 7 jours
- Les cookies sont configurés avec :
- HttpOnly
- SameSite=Lax
- Secure en production
- Path=/
-
Déconnexion
- La session est supprimée de la base de données
- Le cookie est expiré
Si vous rencontrez des problèmes de connexion :
-
Vérifiez les logs
- Les logs détaillés sont disponibles dans la console du navigateur
- Les logs serveur montrent le processus d'authentification
-
Vérifiez les cookies
- Le cookie
turso-token
doit être présent après la connexion - Les options du cookie doivent correspondre à la configuration
- Le cookie
-
Vérifiez la base de données
- L'utilisateur doit exister dans la table
auth_users
- Le mot de passe doit être correctement haché
- La session doit être créée dans la table
sessions
- L'utilisateur doit exister dans la table