- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
11 KiB
Python
269 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Validation des Traductions - RPA Vision V3
|
|
Auteur : Dom, Alice, Kiro - 7 janvier 2026
|
|
|
|
Script pour valider la cohérence et la complétude des fichiers de traduction.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set, Any
|
|
|
|
class TranslationValidator:
|
|
"""
|
|
Validateur pour les fichiers de traduction
|
|
"""
|
|
|
|
def __init__(self, i18n_dir: str = "i18n"):
|
|
self.i18n_dir = Path(i18n_dir)
|
|
self.config = self.load_config()
|
|
self.translations = {}
|
|
self.errors = []
|
|
self.warnings = []
|
|
|
|
def load_config(self) -> Dict[str, Any]:
|
|
"""Charge la configuration i18n"""
|
|
config_path = self.i18n_dir / "config.json"
|
|
try:
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
except Exception as e:
|
|
self.errors.append(f"Erreur lors du chargement de config.json: {e}")
|
|
return {}
|
|
|
|
def load_translations(self) -> None:
|
|
"""Charge tous les fichiers de traduction"""
|
|
if not self.config:
|
|
return
|
|
|
|
for lang in self.config.get('supportedLanguages', []):
|
|
lang_code = lang['code']
|
|
lang_file = self.i18n_dir / f"{lang_code}.json"
|
|
|
|
try:
|
|
with open(lang_file, 'r', encoding='utf-8') as f:
|
|
self.translations[lang_code] = json.load(f)
|
|
print(f"✅ Chargé: {lang_code}.json")
|
|
except FileNotFoundError:
|
|
self.errors.append(f"Fichier manquant: {lang_code}.json")
|
|
except json.JSONDecodeError as e:
|
|
self.errors.append(f"JSON invalide dans {lang_code}.json: {e}")
|
|
except Exception as e:
|
|
self.errors.append(f"Erreur lors du chargement de {lang_code}.json: {e}")
|
|
|
|
def get_all_keys(self, obj: Dict[str, Any], prefix: str = "") -> Set[str]:
|
|
"""Extrait toutes les clés d'un objet JSON imbriqué"""
|
|
keys = set()
|
|
|
|
for key, value in obj.items():
|
|
full_key = f"{prefix}.{key}" if prefix else key
|
|
|
|
if isinstance(value, dict):
|
|
keys.update(self.get_all_keys(value, full_key))
|
|
else:
|
|
keys.add(full_key)
|
|
|
|
return keys
|
|
|
|
def validate_structure(self) -> None:
|
|
"""Valide la structure des fichiers de traduction"""
|
|
if not self.translations:
|
|
self.errors.append("Aucun fichier de traduction chargé")
|
|
return
|
|
|
|
# Obtenir les clés de référence (langue par défaut)
|
|
default_lang = self.config.get('defaultLanguage', 'fr')
|
|
if default_lang not in self.translations:
|
|
self.errors.append(f"Langue par défaut '{default_lang}' non trouvée")
|
|
return
|
|
|
|
reference_keys = self.get_all_keys(self.translations[default_lang])
|
|
print(f"📋 Clés de référence ({default_lang}): {len(reference_keys)}")
|
|
|
|
# Vérifier chaque langue
|
|
for lang_code, translations in self.translations.items():
|
|
if lang_code == default_lang:
|
|
continue
|
|
|
|
lang_keys = self.get_all_keys(translations)
|
|
|
|
# Clés manquantes
|
|
missing_keys = reference_keys - lang_keys
|
|
if missing_keys:
|
|
self.errors.append(f"Clés manquantes dans {lang_code}: {sorted(missing_keys)}")
|
|
|
|
# Clés supplémentaires
|
|
extra_keys = lang_keys - reference_keys
|
|
if extra_keys:
|
|
self.warnings.append(f"Clés supplémentaires dans {lang_code}: {sorted(extra_keys)}")
|
|
|
|
print(f"🔍 {lang_code}: {len(lang_keys)} clés ({len(missing_keys)} manquantes, {len(extra_keys)} supplémentaires)")
|
|
|
|
def validate_placeholders(self) -> None:
|
|
"""Valide les placeholders dans les traductions"""
|
|
import re
|
|
|
|
placeholder_pattern = re.compile(r'\{\{(\w+)\}\}')
|
|
|
|
for lang_code, translations in self.translations.items():
|
|
self._validate_placeholders_recursive(translations, lang_code, placeholder_pattern)
|
|
|
|
def _validate_placeholders_recursive(self, obj: Dict[str, Any], lang_code: str, pattern, prefix: str = "") -> None:
|
|
"""Valide récursivement les placeholders"""
|
|
for key, value in obj.items():
|
|
full_key = f"{prefix}.{key}" if prefix else key
|
|
|
|
if isinstance(value, dict):
|
|
self._validate_placeholders_recursive(value, lang_code, pattern, full_key)
|
|
elif isinstance(value, str):
|
|
placeholders = pattern.findall(value)
|
|
if placeholders:
|
|
# Vérifier la cohérence avec la langue de référence
|
|
default_lang = self.config.get('defaultLanguage', 'fr')
|
|
if lang_code != default_lang and default_lang in self.translations:
|
|
ref_value = self._get_nested_value(self.translations[default_lang], full_key.split('.'))
|
|
if ref_value:
|
|
ref_placeholders = pattern.findall(ref_value)
|
|
if set(placeholders) != set(ref_placeholders):
|
|
self.warnings.append(
|
|
f"Placeholders différents dans {lang_code}.{full_key}: "
|
|
f"{placeholders} vs {ref_placeholders}"
|
|
)
|
|
|
|
def _get_nested_value(self, obj: Dict[str, Any], keys: List[str]) -> str:
|
|
"""Obtient une valeur imbriquée"""
|
|
current = obj
|
|
for key in keys:
|
|
if isinstance(current, dict) and key in current:
|
|
current = current[key]
|
|
else:
|
|
return ""
|
|
return current if isinstance(current, str) else ""
|
|
|
|
def validate_empty_values(self) -> None:
|
|
"""Valide qu'il n'y a pas de valeurs vides"""
|
|
for lang_code, translations in self.translations.items():
|
|
self._validate_empty_recursive(translations, lang_code)
|
|
|
|
def _validate_empty_recursive(self, obj: Dict[str, Any], lang_code: str, prefix: str = "") -> None:
|
|
"""Valide récursivement les valeurs vides"""
|
|
for key, value in obj.items():
|
|
full_key = f"{prefix}.{key}" if prefix else key
|
|
|
|
if isinstance(value, dict):
|
|
self._validate_empty_recursive(value, lang_code, full_key)
|
|
elif isinstance(value, str):
|
|
if not value.strip():
|
|
self.warnings.append(f"Valeur vide dans {lang_code}.{full_key}")
|
|
|
|
def validate_config(self) -> None:
|
|
"""Valide la configuration"""
|
|
required_fields = ['defaultLanguage', 'supportedLanguages', 'fallbackLanguage']
|
|
|
|
for field in required_fields:
|
|
if field not in self.config:
|
|
self.errors.append(f"Champ manquant dans config.json: {field}")
|
|
|
|
# Vérifier les langues supportées
|
|
if 'supportedLanguages' in self.config:
|
|
for i, lang in enumerate(self.config['supportedLanguages']):
|
|
required_lang_fields = ['code', 'name', 'flag', 'currency']
|
|
for field in required_lang_fields:
|
|
if field not in lang:
|
|
self.errors.append(f"Champ manquant dans supportedLanguages[{i}]: {field}")
|
|
|
|
# Vérifier que la langue par défaut est supportée
|
|
default_lang = self.config.get('defaultLanguage')
|
|
supported_codes = [lang['code'] for lang in self.config.get('supportedLanguages', [])]
|
|
|
|
if default_lang and default_lang not in supported_codes:
|
|
self.errors.append(f"Langue par défaut '{default_lang}' non dans supportedLanguages")
|
|
|
|
def generate_report(self) -> str:
|
|
"""Génère un rapport de validation"""
|
|
report = []
|
|
report.append("=" * 60)
|
|
report.append("RAPPORT DE VALIDATION DES TRADUCTIONS")
|
|
report.append("=" * 60)
|
|
report.append("")
|
|
|
|
# Statistiques
|
|
report.append("📊 STATISTIQUES:")
|
|
report.append(f" • Langues configurées: {len(self.config.get('supportedLanguages', []))}")
|
|
report.append(f" • Fichiers chargés: {len(self.translations)}")
|
|
report.append(f" • Erreurs trouvées: {len(self.errors)}")
|
|
report.append(f" • Avertissements: {len(self.warnings)}")
|
|
report.append("")
|
|
|
|
# Erreurs
|
|
if self.errors:
|
|
report.append("❌ ERREURS:")
|
|
for error in self.errors:
|
|
report.append(f" • {error}")
|
|
report.append("")
|
|
|
|
# Avertissements
|
|
if self.warnings:
|
|
report.append("⚠️ AVERTISSEMENTS:")
|
|
for warning in self.warnings:
|
|
report.append(f" • {warning}")
|
|
report.append("")
|
|
|
|
# Résumé
|
|
if not self.errors and not self.warnings:
|
|
report.append("✅ VALIDATION RÉUSSIE: Aucun problème détecté!")
|
|
elif not self.errors:
|
|
report.append("✅ VALIDATION RÉUSSIE: Quelques avertissements mineurs")
|
|
else:
|
|
report.append("❌ VALIDATION ÉCHOUÉE: Des erreurs doivent être corrigées")
|
|
|
|
report.append("")
|
|
report.append("=" * 60)
|
|
|
|
return "\n".join(report)
|
|
|
|
def run_validation(self) -> bool:
|
|
"""Exécute toutes les validations"""
|
|
print("🔍 Démarrage de la validation des traductions...")
|
|
print()
|
|
|
|
# Validation de la configuration
|
|
print("📋 Validation de la configuration...")
|
|
self.validate_config()
|
|
|
|
# Chargement des traductions
|
|
print("📂 Chargement des fichiers de traduction...")
|
|
self.load_translations()
|
|
|
|
if not self.translations:
|
|
print("❌ Aucun fichier de traduction chargé")
|
|
return False
|
|
|
|
# Validations
|
|
print("🔍 Validation de la structure...")
|
|
self.validate_structure()
|
|
|
|
print("🔍 Validation des placeholders...")
|
|
self.validate_placeholders()
|
|
|
|
print("🔍 Validation des valeurs vides...")
|
|
self.validate_empty_values()
|
|
|
|
# Génération du rapport
|
|
print()
|
|
print(self.generate_report())
|
|
|
|
return len(self.errors) == 0
|
|
|
|
def main():
|
|
"""Point d'entrée principal"""
|
|
validator = TranslationValidator()
|
|
success = validator.run_validation()
|
|
|
|
return 0 if success else 1
|
|
|
|
if __name__ == "__main__":
|
|
exit(main()) |