Locales - Fichiers de Traduction
Fichiers JSON des traductions multilingues (nl, fr, de, en).
Localisation : frontend/src/locales/
Structure
locales/
├── nl.json # Nederlands (néerlandais) - Langue par défaut
├── fr.json # Français
├── de.json # Deutsch (allemand)
└── en.json # English (anglais)
Statistiques Belgique :
🇳🇱 60% : Néerlandais (Flandre)
🇫🇷 40% : Français (Wallonie + Bruxelles)
🇩🇪 <1% : Allemand (Communauté germanophone)
🇬🇧 Intl : Anglais (syndics multinationales)
Format JSON
Structure Plate :
{
"section.cle": "Traduction"
}
Conventions de Nommage :
[section].[clé]: ex:nav.dashboard,building.createsnake_case pour clés :
total_unitspastotalUnitsPréfixes :
nav.*,error.*,success.*,button.*
Sections Communes
Boutons Actions
{
"button.create": "Créer",
"button.edit": "Modifier",
"button.delete": "Supprimer",
"button.save": "Enregistrer",
"button.cancel": "Annuler",
"button.close": "Fermer",
"button.search": "Rechercher",
"button.filter": "Filtrer",
"button.export": "Exporter",
"button.download": "Télécharger",
"button.upload": "Téléverser",
"button.refresh": "Rafraîchir"
}
Messages Succès/Erreur
{
"success.saved": "Enregistré avec succès",
"success.created": "Créé avec succès",
"success.updated": "Mis à jour avec succès",
"success.deleted": "Supprimé avec succès",
"error.network": "Erreur réseau",
"error.unauthorized": "Non autorisé",
"error.forbidden": "Accès interdit",
"error.not_found": "Non trouvé",
"error.server": "Erreur serveur",
"error.validation": "Erreur de validation",
"error.unknown": "Erreur inconnue"
}
Entités Domaine
Buildings
{
"building.title": "Immeubles",
"building.create": "Créer un immeuble",
"building.edit": "Modifier l'immeuble",
"building.delete": "Supprimer l'immeuble",
"building.name": "Nom de l'immeuble",
"building.address": "Adresse",
"building.city": "Ville",
"building.postal_code": "Code postal",
"building.country": "Pays",
"building.total_units": "Nombre de lots",
"building.construction_year": "Année de construction",
"building.details": "Détails de l'immeuble"
}
Owners
{
"owner.title": "Copropriétaires",
"owner.create": "Ajouter un copropriétaire",
"owner.first_name": "Prénom",
"owner.last_name": "Nom",
"owner.email": "Email",
"owner.phone": "Téléphone",
"owner.units": "Lots possédés"
}
Units
{
"unit.title": "Lots",
"unit.number": "Numéro de lot",
"unit.floor": "Étage",
"unit.surface_area": "Surface (m²)",
"unit.ownership_share": "Quote-part (‰)",
"unit.type": "Type de lot",
"unit.type.apartment": "Appartement",
"unit.type.parking": "Parking",
"unit.type.storage": "Cave"
}
Expenses
{
"expense.title": "Charges",
"expense.description": "Description",
"expense.amount": "Montant",
"expense.expense_date": "Date de dépense",
"expense.due_date": "Date d'échéance",
"expense.category": "Catégorie",
"expense.category.maintenance": "Entretien",
"expense.category.repair": "Réparation",
"expense.category.insurance": "Assurance",
"expense.category.utilities": "Charges courantes",
"expense.category.management": "Honoraires syndic",
"expense.category.other": "Autre",
"expense.payment_status": "Statut de paiement",
"expense.status.pending": "En attente",
"expense.status.paid": "Payé",
"expense.status.overdue": "En retard",
"expense.status.cancelled": "Annulé"
}
Meetings
{
"meeting.title": "Assemblées Générales",
"meeting.create": "Convoquer une AG",
"meeting.date": "Date de l'assemblée",
"meeting.agenda": "Ordre du jour",
"meeting.minutes": "Procès-verbal",
"meeting.status": "Statut",
"meeting.status.scheduled": "Planifiée",
"meeting.status.completed": "Terminée",
"meeting.status.cancelled": "Annulée"
}
Documents
{
"document.title": "Documents",
"document.upload": "Téléverser un document",
"document.type": "Type de document",
"document.type.pcn": "PCN (Précompte)",
"document.type.regulation": "Règlement",
"document.type.contract": "Contrat",
"document.type.invoice": "Facture",
"document.type.other": "Autre",
"document.file_name": "Nom du fichier",
"document.upload_date": "Date de téléversement"
}
Pagination
{
"pagination.showing": "Affichage",
"pagination.of": "sur",
"pagination.results": "résultats",
"pagination.page": "Page",
"pagination.per_page": "Par page",
"pagination.previous": "Précédent",
"pagination.next": "Suivant",
"pagination.first": "Premier",
"pagination.last": "Dernier"
}
Formulaires
{
"form.required": "Champ requis",
"form.invalid_email": "Email invalide",
"form.invalid_phone": "Téléphone invalide",
"form.min_length": "Minimum {min} caractères",
"form.max_length": "Maximum {max} caractères",
"form.min_value": "Minimum {min}",
"form.max_value": "Maximum {max}"
}
Authentification
{
"auth.login": "Se connecter",
"auth.logout": "Se déconnecter",
"auth.email": "Adresse email",
"auth.password": "Mot de passe",
"auth.forgot_password": "Mot de passe oublié ?",
"auth.welcome_back": "Bienvenue !",
"auth.invalid_credentials": "Identifiants invalides"
}
Paramètres Dynamiques
Avec Variables :
{
"welcome.message": "Bienvenue, {name} !",
"building.units_count": "{count} lot(s)",
"expense.amount_eur": "{amount} €"
}
Utilisation :
<h1>{$_('welcome.message', { values: { name: user.firstName } })}</h1>
<p>{$_('building.units_count', { values: { count: building.total_units } })}</p>
Pluralisation :
{
"building.units_plural": "{count, plural, =0 {aucun lot} one {1 lot} other {# lots}}"
}
<p>{$_('building.units_plural', { values: { count: totalUnits } })}</p>
Dates et Nombres
Format Dates :
{
"date.format.short": "dd/MM/yyyy",
"date.format.long": "dd MMMM yyyy",
"date.today": "Aujourd'hui",
"date.yesterday": "Hier",
"date.tomorrow": "Demain"
}
Utilisation avec svelte-i18n :
<script>
import { date, number } from 'svelte-i18n';
</script>
<p>{$date(new Date(), { format: 'short' })}</p>
<p>{$number(1234.56, { style: 'currency', currency: 'EUR' })}</p>
Maintenance Traductions
Workflow
Ajouter clé dans nl.json (référence)
Traduire dans fr.json, de.json, en.json
Utiliser dans composant :
$_('nouvelle.cle')Tester changement de langue
Outils Recommandés
i18n-ally (VS Code) :
Extension VS Code
Visualisation inline
Détection clés manquantes
Édition multi-langues
Script Vérification :
#!/bin/bash
# scripts/check-i18n.sh
echo "🔍 Vérification traductions..."
# Trouver clés en nl.json
NL_KEYS=$(jq -r 'keys[]' frontend/src/locales/nl.json | sort)
# Vérifier chaque langue
for LANG in fr de en; do
echo "Vérification ${LANG}..."
LANG_KEYS=$(jq -r 'keys[]' frontend/src/locales/${LANG}.json | sort)
MISSING=$(comm -23 <(echo "$NL_KEYS") <(echo "$LANG_KEYS"))
if [ -n "$MISSING" ]; then
echo "⚠️ Clés manquantes dans ${LANG}.json:"
echo "$MISSING"
else
echo "✅ ${LANG}.json complet"
fi
done
Exécution :
chmod +x scripts/check-i18n.sh
./scripts/check-i18n.sh
Exemples Complets
nl.json (Néerlandais)
{
"nav.dashboard": "Dashboard",
"nav.buildings": "Gebouwen",
"nav.owners": "Mede-eigenaars",
"nav.expenses": "Kosten",
"building.title": "Gebouwen",
"building.create": "Gebouw aanmaken",
"building.name": "Gebouwnaam",
"building.address": "Adres",
"building.total_units": "Aantal kavels",
"button.save": "Opslaan",
"button.cancel": "Annuleren",
"success.created": "Succesvol aangemaakt",
"error.network": "Netwerkfout"
}
fr.json (Français)
{
"nav.dashboard": "Tableau de bord",
"nav.buildings": "Immeubles",
"nav.owners": "Copropriétaires",
"nav.expenses": "Charges",
"building.title": "Immeubles",
"building.create": "Créer un immeuble",
"building.name": "Nom de l'immeuble",
"building.address": "Adresse",
"building.total_units": "Nombre de lots",
"button.save": "Enregistrer",
"button.cancel": "Annuler",
"success.created": "Créé avec succès",
"error.network": "Erreur réseau"
}
de.json (Allemand)
{
"nav.dashboard": "Dashboard",
"nav.buildings": "Gebäude",
"nav.owners": "Miteigentümer",
"nav.expenses": "Kosten",
"building.title": "Gebäude",
"building.create": "Gebäude erstellen",
"building.name": "Gebäudename",
"building.address": "Adresse",
"building.total_units": "Anzahl Einheiten",
"button.save": "Speichern",
"button.cancel": "Abbrechen",
"success.created": "Erfolgreich erstellt",
"error.network": "Netzwerkfehler"
}
en.json (Anglais)
{
"nav.dashboard": "Dashboard",
"nav.buildings": "Buildings",
"nav.owners": "Co-owners",
"nav.expenses": "Expenses",
"building.title": "Buildings",
"building.create": "Create building",
"building.name": "Building name",
"building.address": "Address",
"building.total_units": "Number of units",
"button.save": "Save",
"button.cancel": "Cancel",
"success.created": "Successfully created",
"error.network": "Network error"
}
Tests i18n
// tests/unit/locales.test.ts
import { describe, it, expect } from 'vitest';
import nl from '../src/locales/nl.json';
import fr from '../src/locales/fr.json';
import de from '../src/locales/de.json';
import en from '../src/locales/en.json';
describe('i18n completeness', () => {
const nlKeys = Object.keys(nl);
it('fr.json should have all keys', () => {
const frKeys = Object.keys(fr);
expect(frKeys).toEqual(nlKeys);
});
it('de.json should have all keys', () => {
const deKeys = Object.keys(de);
expect(deKeys).toEqual(nlKeys);
});
it('en.json should have all keys', () => {
const enKeys = Object.keys(en);
expect(enKeys).toEqual(nlKeys);
});
});
Bonnes Pratiques
nl.json comme Référence :
Toujours ajouter clés en néerlandais d’abord.
Traductions Professionnelles :
Éviter Google Translate pour textes importants.
Contexte dans Clés :
button.savevsform.save(contexte clair).Pas de Code dans Traductions :
❌
"text": "Cliquez <a href='...'>ici</a>"✅ Utiliser composants SvelteVariables Explicites :
{name}pas{0}
Références
Configuration i18n :
frontend/src/lib/i18n.tsLanguageSelector :
frontend/src/components/LanguageSelector.sveltesvelte-i18n Docs : https://github.com/kaisermann/svelte-i18n