#!/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())