From 279cf3e44391c1774e858c9736afaa034dd39be3 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 03:56:43 +0000 Subject: [PATCH] Add source files --- i18n_key_sync/report.py | 408 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 i18n_key_sync/report.py diff --git a/i18n_key_sync/report.py b/i18n_key_sync/report.py new file mode 100644 index 0000000..bdd561c --- /dev/null +++ b/i18n_key_sync/report.py @@ -0,0 +1,408 @@ +"""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)