1
0
Fork 0
mirror of https://github.com/EdgeTX/edgetx.git synced 2025-07-13 11:29:49 +03:00
edgetx/tools/check_translations.py

351 lines
14 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Translation Checker for EdgeTX Translations
This script can check:
1. Bootloader translations (bl_translations.h) - unified file with multiple languages
2. Individual language translation files (*.h) - separate files per language
It ensures that all translation languages have the same set of translation strings defined.
"""
import re
import sys
import os
from pathlib import Path
from collections import defaultdict
from typing import Dict, Set, List, Tuple, Optional
import argparse
import glob
class TranslationChecker:
def __init__(self):
self.bootloader_translations = defaultdict(set) # language -> set of bootloader translation keys
self.language_translations = defaultdict(set) # language -> set of language translation keys
self.bootloader_keys = set()
self.language_keys = set()
self.checked_files = []
def find_translations_directory(self, start_path: str) -> Optional[Path]:
"""Find the translations directory by searching up from the given path."""
current_path = Path(start_path).resolve()
# If the provided path is already the translations directory
if current_path.name == "translations" and current_path.is_dir():
return current_path
# If the provided path is a file in translations directory
if current_path.parent.name == "translations":
return current_path.parent
# Search up the directory tree
while current_path != current_path.parent:
translations_path = current_path / "radio" / "src" / "translations"
if translations_path.exists() and translations_path.is_dir():
return translations_path
current_path = current_path.parent
return None
def parse_bootloader_file(self, file_path: Path):
"""Parse the bootloader translation file (bl_translations.h)."""
if not file_path.exists():
print(f"Warning: File {file_path} does not exist")
return False
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Split content into lines for easier processing
lines = content.split('\n')
current_language = None
in_translation_block = False
conditional_depth = 0
for i, line in enumerate(lines):
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith('//'):
continue
# Check for translation language blocks
translation_match = re.match(r'#(?:if|elif)\s+defined\(TRANSLATIONS_([A-Z]+)\)', line)
if translation_match:
current_language = translation_match.group(1)
in_translation_block = True
conditional_depth = 0
continue
# Check for else block (default/English)
if re.match(r'#else', line) and in_translation_block and conditional_depth == 0:
current_language = "EN" # Default language
continue
# Track conditional compilation depth
if re.match(r'#if', line) and in_translation_block:
conditional_depth += 1
continue
elif re.match(r'#endif', line) and in_translation_block:
if conditional_depth > 0:
conditional_depth -= 1
else:
# This is the end of the translation block
in_translation_block = False
current_language = None
continue
# Skip non-translation lines
if not in_translation_block or not current_language:
continue
# Parse #define statements
define_match = re.match(r'#define\s+(TR_BL_\w+)', line)
if define_match:
key = define_match.group(1)
self.bootloader_translations[current_language].add(key)
self.bootloader_keys.add(key)
self.checked_files.append(str(file_path))
return True
def parse_language_file(self, file_path: Path) -> Optional[str]:
"""Parse an individual language translation file (e.g., en.h, fr.h)."""
if not file_path.exists():
return None
# Extract language code from filename
language = file_path.stem.upper()
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Find all #define TR_ statements
define_matches = re.findall(r'#define\s+(TR_\w+)', content)
for key in define_matches:
self.language_translations[language].add(key)
self.language_keys.add(key)
self.checked_files.append(str(file_path))
return language
def check_bootloader_translations(self, translations_dir: Path) -> bool:
"""Check bootloader translations."""
bl_file = translations_dir / "bl_translations.h"
return self.parse_bootloader_file(bl_file)
def check_language_translations(self, translations_dir: Path) -> List[str]:
"""Check individual language translation files."""
languages_found = []
# Look for .h files that are language files (excluding special files)
exclude_files = {"bl_translations.h", "untranslated.h"}
for h_file in translations_dir.glob("*.h"):
if h_file.name in exclude_files:
continue
# Skip files that are clearly not language files
if h_file.name.startswith("tts_"):
continue
language = self.parse_language_file(h_file)
if language:
languages_found.append(language)
return languages_found
def analyze(self) -> Dict[str, any]:
"""Analyze translations and return results."""
bootloader_languages = list(self.bootloader_translations.keys())
language_languages = list(self.language_translations.keys())
results = {
"bootloader": {
"languages": bootloader_languages,
"total_keys": len(self.bootloader_keys),
"missing_keys": defaultdict(set),
"extra_keys": defaultdict(set),
"summary": {}
},
"language_files": {
"languages": language_languages,
"total_keys": len(self.language_keys),
"missing_keys": defaultdict(set),
"extra_keys": defaultdict(set),
"summary": {}
},
"checked_files": self.checked_files
}
# Analyze bootloader translations
for lang in bootloader_languages:
lang_keys = self.bootloader_translations[lang]
results["bootloader"]["missing_keys"][lang] = self.bootloader_keys - lang_keys
results["bootloader"]["extra_keys"][lang] = lang_keys - self.bootloader_keys
results["bootloader"]["summary"][lang] = {
"total": len(lang_keys),
"missing": len(results["bootloader"]["missing_keys"][lang]),
"extra": len(results["bootloader"]["extra_keys"][lang])
}
# Analyze language file translations
for lang in language_languages:
lang_keys = self.language_translations[lang]
results["language_files"]["missing_keys"][lang] = self.language_keys - lang_keys
results["language_files"]["extra_keys"][lang] = lang_keys - self.language_keys
results["language_files"]["summary"][lang] = {
"total": len(lang_keys),
"missing": len(results["language_files"]["missing_keys"][lang]),
"extra": len(results["language_files"]["extra_keys"][lang])
}
return results
def print_report(self, verbose=False):
"""Print a detailed report of the translation analysis."""
results = self.analyze()
has_bootloader = bool(results["bootloader"]["languages"])
has_language_files = bool(results["language_files"]["languages"])
if not has_bootloader and not has_language_files:
print("No translation files found or processed.")
return
print("EdgeTX Translation Analysis")
print("=" * 50)
if verbose:
print(f"Checked files: {len(results['checked_files'])}")
for file_path in results['checked_files']:
print(f" - {file_path}")
print()
# Report bootloader translations
if has_bootloader:
self._print_section_report("Bootloader Translations (bl_translations.h)",
results["bootloader"], self.bootloader_keys, verbose)
# Report language file translations
if has_language_files:
self._print_section_report("Language File Translations (*.h)",
results["language_files"], self.language_keys, verbose)
def _print_section_report(self, title: str, section_results: Dict, all_keys: Set, verbose: bool):
"""Print report for a specific section (bootloader or language files)."""
print(f"\n{title}")
print("-" * len(title))
print(f"Total unique translation keys: {section_results['total_keys']}")
print(f"Languages found: {', '.join(sorted(section_results['languages']))}")
print()
# Summary table
print("Summary by Language:")
print("-" * 60)
print(f"{'Language':<12} {'Total':<8} {'Missing':<10} {'Extra':<8}")
print("-" * 60)
for lang in sorted(section_results['languages']):
summary = section_results['summary'][lang]
print(f"{lang:<12} {summary['total']:<8} {summary['missing']:<10} {summary['extra']:<8}")
print()
# Detailed missing keys report
has_issues = False
for lang in sorted(section_results['languages']):
missing = section_results['missing_keys'][lang]
extra = section_results['extra_keys'][lang]
if missing or extra:
has_issues = True
print(f"Issues for {lang}:")
if missing:
print(f" Missing keys ({len(missing)}):")
for key in sorted(missing):
print(f" - {key}")
if extra:
print(f" Extra keys ({len(extra)}):")
for key in sorted(extra):
print(f" + {key}")
print()
if not has_issues:
print("✅ All languages have consistent translation keys!")
else:
print("❌ Translation inconsistencies found!")
# Show all translation keys for reference (only if verbose)
if verbose:
print(f"\nAll Translation Keys ({len(all_keys)}):")
print("-" * 40)
for key in sorted(all_keys):
print(f" {key}")
print()
def main():
parser = argparse.ArgumentParser(
description="Check EdgeTX translation consistency",
epilog="""
Examples:
# Check bootloader translations only
python3 check_translations.py --bootloader
# Check individual language files only
python3 check_translations.py --languages
# Check both (default)
python3 check_translations.py
# Check with verbose output
python3 check_translations.py -v
# Specify path to translations directory or EdgeTX root
python3 check_translations.py /path/to/edgetx/radio/src/translations
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("path", nargs="?", default=".",
help="Path to translations directory, EdgeTX root, or current directory")
parser.add_argument("-v", "--verbose", action="store_true",
help="Show all translation keys")
parser.add_argument("--bootloader", action="store_true",
help="Check only bootloader translations (bl_translations.h)")
parser.add_argument("--languages", action="store_true",
help="Check only individual language files (*.h)")
args = parser.parse_args()
checker = TranslationChecker()
# Find translations directory
translations_dir = checker.find_translations_directory(args.path)
if not translations_dir:
print(f"Error: Could not find translations directory from path: {args.path}")
print("Please specify a path to the EdgeTX repository root or translations directory.")
sys.exit(1)
print(f"Using translations directory: {translations_dir}")
print()
# Determine what to check
check_bootloader = args.bootloader or not args.languages
check_languages = args.languages or not args.bootloader
if check_bootloader:
if not checker.check_bootloader_translations(translations_dir):
print("Warning: Could not process bootloader translations")
if check_languages:
languages_found = checker.check_language_translations(translations_dir)
if not languages_found:
print("Warning: No individual language translation files found")
checker.print_report(verbose=args.verbose)
if __name__ == "__main__":
main()