mirror of
https://github.com/EdgeTX/edgetx.git
synced 2025-07-13 11:29:49 +03:00
351 lines
14 KiB
Python
Executable file
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()
|