v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- 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>
This commit is contained in:
269
i18n/validate_translations.py
Normal file
269
i18n/validate_translations.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user