mirror of
https://github.com/EdgeTX/edgetx.git
synced 2025-07-25 09:15:21 +03:00
chore: make helper tools more robust, translation helpers (#6396)
This commit is contained in:
parent
c31b54041b
commit
112df45521
5 changed files with 645 additions and 113 deletions
351
tools/check_translations.py
Executable file
351
tools/check_translations.py
Executable file
|
@ -0,0 +1,351 @@
|
|||
#!/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()
|
Loading…
Add table
Add a link
Reference in a new issue