"""Reporting for i18n key analysis.""" from typing import Dict, Set from rich.console import Console from rich.table import Table from rich.text import Text from i18n_key_sync.validate import ValidationResult class Reporter: """Generate reports for i18n key analysis.""" def __init__(self): """Initialize the reporter.""" self.console = Console() def print_validation_result( self, result: ValidationResult, format: str = "rich", ) -> None: """Print validation result to console. Args: result: ValidationResult to display. format: Output format (rich, minimal, json). """ if format == "json": self._print_json_result(result) elif format == "minimal": self._print_minimal_result(result) else: self._print_rich_result(result) def _print_rich_result(self, result: ValidationResult) -> None: """Print rich-formatted validation result. Args: result: ValidationResult to display. """ if result.missing_keys: table = Table(title="Missing Keys (in code but not in locale)") table.add_column("Key", style="red") for key in sorted(result.missing_keys): table.add_row(key) self.console.print(table) self.console.print(f"\n[red]Total missing: {len(result.missing_keys)}[/red]\n") if result.unused_keys: table = Table(title="Unused Keys (in locale but not in code)") table.add_column("Key", style="yellow") for key in sorted(result.unused_keys): table.add_row(key) self.console.print(table) self.console.print(f"\n[yellow]Total unused: {len(result.unused_keys)}[/yellow]\n") if result.matching_keys: self.console.print(f"[green]Matching keys: {len(result.matching_keys)}[/green]") if result.is_valid: self.console.print("[green]✓ All keys are valid![/green]") def _print_json_result(self, result: ValidationResult) -> None: """Print JSON-formatted validation result. Args: result: ValidationResult to display. """ import json output = { "missing_keys": sorted(list(result.missing_keys)), "unused_keys": sorted(list(result.unused_keys)), "matching_keys": sorted(list(result.matching_keys)), "missing_count": len(result.missing_keys), "unused_count": len(result.unused_keys), "matching_count": len(result.matching_keys), "is_valid": result.is_valid, } self.console.print(json.dumps(output, indent=2)) def _print_minimal_result(self, result: ValidationResult) -> None: """Print minimal validation result. Args: result: ValidationResult to display. """ if result.missing_keys: self.console.print(f"MISSING: {', '.join(sorted(result.missing_keys))}") if result.unused_keys: self.console.print(f"UNUSED: {', '.join(sorted(result.unused_keys))}") if result.is_valid: self.console.print("OK") def print_sync_changes( self, changes: list, dry_run: bool = False, ) -> None: """Print sync changes to console. Args: changes: List of SyncChange objects. dry_run: Whether this was a dry run. """ if dry_run: self.console.print("[yellow]DRY RUN - No files were modified[/yellow]\n") if not changes: self.console.print("No changes needed.") return if dry_run: self.console.print(f"Would make {len(changes)} change(s):\n") else: self.console.print(f"Made {len(changes)} change(s):\n") changes_by_locale: dict = {} for change in changes: if change.locale not in changes_by_locale: changes_by_locale[change.locale] = [] changes_by_locale[change.locale].append(change) for locale, locale_changes in sorted(changes_by_locale.items()): table = Table(title=f"Locale: {locale}") table.add_column("Key") table.add_column("Action") table.add_column("Value") for change in locale_changes: action_style = "green" if change.action == "add" else "yellow" value = str(change.new_value) if change.new_value else "(removed)" table.add_row( change.key, Text(change.action.upper(), style=action_style), value, ) self.console.print(table) self.console.print() def generate_report( self, extracted_keys: Set[str], locale_keys: Dict[str, Set[str]], format: str = "rich", include_details: bool = False, coverage_threshold: float = 0.0, ) -> str: """Generate a full i18n report. Args: extracted_keys: Set of keys extracted from source code. locale_keys: Dictionary mapping locales to their key sets. format: Output format (rich, json, markdown). include_details: Whether to include detailed key lists. coverage_threshold: Coverage threshold for warnings. Returns: Report content as string. """ if format == "json": return self._generate_json_report( extracted_keys, locale_keys, include_details, coverage_threshold, ) elif format == "markdown": return self._generate_markdown_report( extracted_keys, locale_keys, include_details, coverage_threshold, ) else: return self._generate_rich_report( extracted_keys, locale_keys, include_details, coverage_threshold, ) def _generate_rich_report( self, extracted_keys: Set[str], locale_keys: Dict[str, Set[str]], include_details: bool, coverage_threshold: float, ) -> str: """Generate a rich-formatted report. Args: extracted_keys: Set of keys extracted from source code. locale_keys: Dictionary mapping locales to their key sets. include_details: Whether to include detailed key lists. coverage_threshold: Coverage threshold for warnings. Returns: Report content as string. """ from io import StringIO output = StringIO() console = Console(file=output, force_terminal=True) console.print("\n[bold]i18n Key Report[/bold]\n") total_code_keys = len(extracted_keys) summary_table = Table(title="Summary") summary_table.add_column("Locale") summary_table.add_column("Keys", justify="right") summary_table.add_column("Missing", justify="right") summary_table.add_column("Unused", justify="right") summary_table.add_column("Coverage", justify="right") all_missing = set() all_unused = set() for locale, keys in sorted(locale_keys.items()): missing = extracted_keys - keys unused = keys - extracted_keys all_missing.update(missing) all_unused.update(unused) if total_code_keys > 0: coverage = (len(keys & extracted_keys) / total_code_keys) * 100 coverage_str = f"{coverage:.1f}%" if coverage < coverage_threshold: coverage_str = f"[red]{coverage_str}[/red]" else: coverage_str = "N/A" summary_table.add_row( locale, str(len(keys)), str(len(missing)), str(len(unused)), coverage_str, ) console.print(summary_table) if include_details: if all_missing: console.print("\n[red bold]Missing Keys[/red bold]") for locale, keys in sorted(locale_keys.items()): missing = extracted_keys - keys if missing: console.print(f"\n[red]{locale}:[/red]") for key in sorted(missing): console.print(f" - {key}") if all_unused: console.print("\n[yellow bold]Unused Keys[/yellow bold]") for locale, keys in sorted(locale_keys.items()): unused = keys - extracted_keys if unused: console.print(f"\n[yellow]{locale}:[/yellow]") for key in sorted(unused): console.print(f" - {key}") console.print(f"\nTotal keys in code: {total_code_keys}") console.print(f"Total missing across all locales: {len(all_missing)}") console.print(f"Total unused across all locales: {len(all_unused)}") return output.getvalue() def _generate_json_report( self, extracted_keys: Set[str], locale_keys: Dict[str, Set[str]], include_details: bool, coverage_threshold: float, ) -> str: """Generate a JSON-formatted report. Args: extracted_keys: Set of keys extracted from source code. locale_keys: Dictionary mapping locales to their key sets. include_details: Whether to include detailed key lists. coverage_threshold: Coverage threshold for warnings. Returns: Report content as string. """ import json total_code_keys = len(extracted_keys) all_missing = set() all_unused = set() locales_data = {} for locale, keys in sorted(locale_keys.items()): missing = extracted_keys - keys unused = keys - extracted_keys all_missing.update(missing) all_unused.update(unused) if total_code_keys > 0: coverage = (len(keys & extracted_keys) / total_code_keys) * 100 else: coverage = 100.0 locales_data[locale] = { "total_keys": len(keys), "missing_keys": sorted(list(missing)), "unused_keys": sorted(list(unused)), "coverage_percent": round(coverage, 2), } output = { "summary": { "total_keys_in_code": total_code_keys, "total_locales": len(locale_keys), "total_missing": len(all_missing), "total_unused": len(all_unused), "locales": locales_data, }, } if include_details: output["details"] = { "all_missing_keys": sorted(list(all_missing)), "all_unused_keys": sorted(list(all_unused)), } return json.dumps(output, indent=2) def _generate_markdown_report( self, extracted_keys: Set[str], locale_keys: Dict[str, Set[str]], include_details: bool, coverage_threshold: float, ) -> str: """Generate a markdown-formatted report. Args: extracted_keys: Set of keys extracted from source code. locale_keys: Dictionary mapping locales to their key sets. include_details: Whether to include detailed key lists. coverage_threshold: Coverage threshold for warnings. Returns: Report content as string. """ lines = ["# i18n Key Report\n"] total_code_keys = len(extracted_keys) all_missing = set() all_unused = set() lines.append("## Summary\n") lines.append("| Locale | Keys | Missing | Unused | Coverage |") lines.append("|--------|------|---------|--------|----------|") for locale, keys in sorted(locale_keys.items()): missing = extracted_keys - keys unused = keys - extracted_keys all_missing.update(missing) all_unused.update(unused) if total_code_keys > 0: coverage = (len(keys & extracted_keys) / total_code_keys) * 100 coverage_str = f"{coverage:.1f}%" else: coverage_str = "N/A" lines.append(f"| {locale} | {len(keys)} | {len(missing)} | {len(unused)} | {coverage_str} |") lines.append("") lines.append(f"- **Total keys in code**: {total_code_keys}") lines.append(f"- **Total missing across all locales**: {len(all_missing)}") lines.append(f"- **Total unused across all locales**: {len(all_unused)}") if include_details: if all_missing: lines.append("\n## Missing Keys\n") for locale, keys in sorted(locale_keys.items()): missing = extracted_keys - keys if missing: lines.append(f"\n### {locale}\n") for key in sorted(missing): lines.append(f"- `{key}`") if all_unused: lines.append("\n## Unused Keys\n") for locale, keys in sorted(locale_keys.items()): unused = keys - extracted_keys if unused: lines.append(f"\n### {locale}\n") for key in sorted(unused): lines.append(f"- `{key}`") return "\n".join(lines)