Files
Stirling-PDF/scripts/translations/validate_json_structure.py
T
Ludy87 c835f2da06 Update Ruff and lint cleanup
Bump Python tooling to Ruff 0.16.2 and datamodel-code-generator 0.72.3, add the py313 target in pre-commit, and refresh the engine lockfile. This also narrows broad exception handling and applies small Python cleanups across the engine and helper scripts to satisfy newer lint rules without changing runtime behavior.
2026-08-19 00:26:55 +02:00

210 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""
Validate TOML structure and formatting of translation files.
Checks for:
- Valid TOML syntax
- Consistent key structure with en-US
- Missing keys
- Extra keys not in en-US
- Malformed entries
Usage:
python scripts/translations/validate_json_structure.py [--language LANG]
"""
import argparse
import json
import sys
import tomllib # Python 3.11+ (stdlib)
from pathlib import Path
def get_all_keys(d: dict, parent_key: str = "", sep: str = ".") -> set[str]:
"""Get all keys from nested dict as dot-notation paths."""
keys = set()
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
keys.add(new_key)
if isinstance(v, dict):
keys.update(get_all_keys(v, new_key, sep=sep))
return keys
def validate_translation_file(file_path: Path) -> tuple[bool, str]:
"""Validate that a file contains valid TOML."""
try:
with open(file_path, "rb") as f:
tomllib.load(f)
return True, "Valid TOML"
except (OSError, tomllib.TOMLDecodeError) as e:
return False, f"Error reading file: {e!s}"
def validate_structure(en_us_keys: set[str], lang_keys: set[str], lang_code: str) -> dict:
"""Compare structure between en-US and target language."""
missing_keys = en_us_keys - lang_keys
extra_keys = lang_keys - en_us_keys
return {
"language": lang_code,
"missing_keys": sorted(missing_keys),
"extra_keys": sorted(extra_keys),
"total_keys": len(lang_keys),
"expected_keys": len(en_us_keys),
"missing_count": len(missing_keys),
"extra_count": len(extra_keys),
}
def print_validation_result(result: dict, verbose: bool = False):
"""Print validation results in readable format."""
lang = result["language"]
print(f"\n{'=' * 100}")
print(f"Language: {lang}")
print(f"{'=' * 100}")
print(f" Total keys: {result['total_keys']}")
print(f" Expected keys (en-US): {result['expected_keys']}")
print(f" Missing keys: {result['missing_count']}")
print(f" Extra keys: {result['extra_count']}")
if result["missing_count"] == 0 and result["extra_count"] == 0:
print(" ✅ Structure matches en-US perfectly!")
else:
if result["missing_count"] > 0:
print(f"\n ⚠️ Missing {result['missing_count']} key(s):")
if verbose or result["missing_count"] <= 20:
for key in result["missing_keys"][:50]:
print(f" - {key}")
if result["missing_count"] > 50:
print(f" ... and {result['missing_count'] - 50} more")
else:
print(" (use --verbose to see all)")
if result["extra_count"] > 0:
print(f"\n ⚠️ Extra {result['extra_count']} key(s) not in en-US:")
if verbose or result["extra_count"] <= 20:
for key in result["extra_keys"][:50]:
print(f" - {key}")
if result["extra_count"] > 50:
print(f" ... and {result['extra_count'] - 50} more")
else:
print(" (use --verbose to see all)")
print("-" * 100)
def load_translation_file(file_path: Path) -> dict:
"""Load TOML translation file."""
with open(file_path, "rb") as f:
return tomllib.load(f)
def main():
parser = argparse.ArgumentParser(description="Validate translation TOML structure")
parser.add_argument(
"--language",
help="Specific language code to validate (e.g., es-ES)",
default=None,
)
parser.add_argument("--verbose", "-v", action="store_true", help="Show all missing/extra keys")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()
# Define paths
locales_dir = Path("frontend/editor/public/locales")
en_us_path = locales_dir / "en-US" / "translation.toml"
if not en_us_path.exists():
print(f"❌ Error: en-US translation file not found at {en_us_path}")
sys.exit(1)
# Validate en-US itself
is_valid, message = validate_translation_file(en_us_path)
if not is_valid:
print(f"❌ Error in en-US file: {message}")
sys.exit(1)
# Load en-US structure
en_us = load_translation_file(en_us_path)
en_us_keys = get_all_keys(en_us)
# Get list of languages to validate
if args.language:
languages = [args.language]
else:
# Validate all languages except en-US
languages = []
for d in locales_dir.iterdir():
if d.is_dir() and d.name != "en-US" and (d / "translation.toml").exists():
languages.append(d.name)
results = []
json_errors = []
# Validate each language
for lang_code in sorted(languages):
lang_path = locales_dir / lang_code / "translation.toml"
if not lang_path.exists():
print(f"⚠️ Warning: {lang_code}/translation.toml not found, skipping")
continue
# First check if file is valid
is_valid, message = validate_translation_file(lang_path)
if not is_valid:
json_errors.append({"language": lang_code, "file": str(lang_path), "error": message})
continue
# Load and compare structure
lang_data = load_translation_file(lang_path)
lang_keys = get_all_keys(lang_data)
result = validate_structure(en_us_keys, lang_keys, lang_code)
results.append(result)
# Output results
if args.json:
output = {"json_errors": json_errors, "structure_validation": results}
print(json.dumps(output, indent=2, ensure_ascii=False))
else:
# Print syntax errors first
if json_errors:
print("\n❌ Syntax Errors:")
print("=" * 100)
for error in json_errors:
print(f"\nLanguage: {error['language']}")
print(f"File: {error['file']}")
print(f"Error: {error['error']}")
print("\n")
# Print structure validation results
if results:
print("\n📊 Structure Validation Summary:")
print(f" Languages validated: {len(results)}")
perfect = sum(1 for r in results if r["missing_count"] == 0 and r["extra_count"] == 0)
print(f" Perfect matches: {perfect}/{len(results)}")
total_missing = sum(r["missing_count"] for r in results)
total_extra = sum(r["extra_count"] for r in results)
print(f" Total missing keys: {total_missing}")
print(f" Total extra keys: {total_extra}")
for result in results:
print_validation_result(result, verbose=args.verbose)
if not json_errors and perfect == len(results):
print("\n✅ All translations have perfect structure!")
# Exit with error code if issues found
has_issues = len(json_errors) > 0 or any(r["missing_count"] > 0 or r["extra_count"] > 0 for r in results)
sys.exit(1 if has_issues else 0)
if __name__ == "__main__":
main()