Add source files
Some checks failed
CI / test (push) Failing after 10s

This commit is contained in:
2026-02-02 03:56:43 +00:00
parent 5c7c886454
commit 279cf3e443

408
i18n_key_sync/report.py Normal file
View File

@@ -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)