mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Add check_translations and validate_translations scripts
This commit is contained in:
173
scripts/check_translations.py
Executable file
173
scripts/check_translations.py
Executable file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Translation check script for OmniRoute.
|
||||
Checks if all translation keys used in code exist in en.json.
|
||||
|
||||
Usage:
|
||||
python scripts/check_translations.py
|
||||
python scripts/check_translations.py --verbose
|
||||
python scripts/check_translations.py --fix
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
|
||||
def get_namespaces_in_code(src_dir='src'):
|
||||
"""Find all namespaces used in code via useTranslations()."""
|
||||
used_ns = set()
|
||||
for root, dirs, files in os.walk(src_dir):
|
||||
for f in files:
|
||||
if not (f.endswith('.tsx') or f.endswith('.ts')):
|
||||
continue
|
||||
try:
|
||||
content = open(os.path.join(root, f), 'r', encoding='utf-8').read()
|
||||
matches = re.findall(r'useTranslations\(["\']+([^"\']+)["\']+\)', content)
|
||||
used_ns.update(matches)
|
||||
except (IOError, UnicodeDecodeError) as e:
|
||||
print(f"Warning: could not process file {os.path.join(root, f)}: {e}", file=sys.stderr)
|
||||
return used_ns
|
||||
|
||||
|
||||
def get_keys_in_json(json_path):
|
||||
"""Get all keys (including nested) from a JSON file."""
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
keys = set()
|
||||
|
||||
def traverse(obj, prefix=''):
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
key = f"{prefix}.{k}" if prefix else k
|
||||
keys.add(key)
|
||||
if isinstance(v, dict):
|
||||
traverse(v, key)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
traverse(item, prefix)
|
||||
|
||||
traverse(data)
|
||||
return keys
|
||||
|
||||
|
||||
def check_translations(src_dir='src', en_json_path='src/i18n/messages/en.json', verbose=False):
|
||||
"""Check if all translation keys used in code exist in en.json."""
|
||||
# Get namespaces used in code
|
||||
used_ns = get_namespaces_in_code(src_dir)
|
||||
|
||||
# Get namespaces in en.json
|
||||
with open(en_json_path, 'r', encoding='utf-8') as f:
|
||||
en_data = json.load(f)
|
||||
en_ns = set(en_data.keys())
|
||||
|
||||
# Find missing namespaces
|
||||
missing_ns = sorted(used_ns - en_ns)
|
||||
|
||||
# Get all keys from en.json
|
||||
en_keys = get_keys_in_json(en_json_path)
|
||||
|
||||
# Get all keys used in code
|
||||
used_keys = set()
|
||||
for root, dirs, files in os.walk(src_dir):
|
||||
for f in files:
|
||||
if not (f.endswith('.tsx') or f.endswith('.ts')):
|
||||
continue
|
||||
try:
|
||||
content = open(os.path.join(root, f), 'r', encoding='utf-8').read()
|
||||
matches = re.findall(r't\([\'"]+([^\'")]+)[\'"]+\)', content)
|
||||
used_keys.update(matches)
|
||||
except (IOError, UnicodeDecodeError) as e:
|
||||
print(f"Warning: could not process file {os.path.join(root, f)}: {e}", file=sys.stderr)
|
||||
|
||||
# Filter out non-translation keys
|
||||
# Note: check if key IS a path or ends with extension, not just contains it
|
||||
# e.g., "invoice.ts.description" (ts = timestamp) should NOT be filtered
|
||||
# but "components/Button.tsx" or "utils.ts" should be
|
||||
def is_likely_file_path(key: str) -> bool:
|
||||
if key.endswith('.ts') or key.endswith('.tsx') or key.endswith('.js') or key.endswith('.json'):
|
||||
return True
|
||||
if '/.ts' in key or '/.tsx' in key or '/.js' in key or '/.json' in key:
|
||||
return True
|
||||
if '\\.ts' in key or '\\.tsx' in key or '\\.js' in key or '\\.json' in key:
|
||||
return True
|
||||
return False
|
||||
|
||||
filtered = {k for k in used_keys if len(k) > 1
|
||||
and not any(x in k for x in ['../', '@', '\\', '#', '?'])
|
||||
and not is_likely_file_path(k)
|
||||
and k not in ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH']
|
||||
and not k.startswith('#')
|
||||
and k not in [',', '-', '.', ':', '?', ' ', '']}
|
||||
|
||||
# Find missing keys
|
||||
real_missing = []
|
||||
for k in filtered:
|
||||
if k in en_keys:
|
||||
continue
|
||||
|
||||
# Check if key matches any full key in en.json
|
||||
matched = False
|
||||
for ek in en_keys:
|
||||
if k in ek or ek.endswith(k):
|
||||
matched = True
|
||||
break
|
||||
|
||||
if matched:
|
||||
continue
|
||||
|
||||
# Filter out paths and obvious non-translations
|
||||
if k.startswith('./') or k.startswith('/') or k.startswith('x-') or k.startswith('user-'):
|
||||
continue
|
||||
# Use same logic - check if key IS a path or ends with extension
|
||||
if is_likely_file_path(k):
|
||||
continue
|
||||
if k in ['Authorization', 'Content-Disposition', 'IOPlatformUUID', 'REG_SZ']:
|
||||
continue
|
||||
|
||||
real_missing.append(k)
|
||||
|
||||
return missing_ns, sorted(real_missing)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Check translation keys in en.json')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Show detailed output')
|
||||
parser.add_argument('--fix', action='store_true', help='Generate fix suggestions')
|
||||
parser.add_argument('--src', default='src', help='Source directory')
|
||||
parser.add_argument('--json', default='src/i18n/messages/en.json', help='Path to en.json')
|
||||
args = parser.parse_args()
|
||||
|
||||
missing_ns, missing_keys = check_translations(args.src, args.json, args.verbose)
|
||||
|
||||
has_issues = False
|
||||
|
||||
if missing_ns:
|
||||
has_issues = True
|
||||
print("=== MISSING NAMESPACES ===")
|
||||
for ns in missing_ns:
|
||||
print(f" - {ns}")
|
||||
if args.fix:
|
||||
print(f" → Add '{ns}' section to en.json")
|
||||
|
||||
if missing_keys:
|
||||
has_issues = True
|
||||
print("\n=== MISSING TRANSLATION KEYS ===")
|
||||
for k in missing_keys:
|
||||
print(f" - {k}")
|
||||
if args.fix:
|
||||
print(f" → Add to appropriate namespace in en.json")
|
||||
|
||||
if not has_issues:
|
||||
print("✓ All translation namespaces and keys are present in en.json")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"\nTotal: {len(missing_ns)} namespace(s), {len(missing_keys)} key(s) missing")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
519
scripts/validate_translation.py
Executable file
519
scripts/validate_translation.py
Executable file
@@ -0,0 +1,519 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OmniRoute i18n Translation Validator
|
||||
Script for comparing source (en.json) with Czech translation (cs.json)
|
||||
Detects missing translations and source changes needing updates
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Tuple, Any
|
||||
|
||||
# Colors (ANSI)
|
||||
RED = '\033[0;31m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
BLUE = '\033[0;34m'
|
||||
NC = '\033[0m'
|
||||
|
||||
# Configuration - find repo root relative to this script
|
||||
_script_dir = Path(__file__).parent.resolve()
|
||||
# If script is in scripts/ subfolder, go up one level to repo root
|
||||
if _script_dir.name == "scripts":
|
||||
SCRIPT_DIR = _script_dir.parent
|
||||
else:
|
||||
SCRIPT_DIR = _script_dir
|
||||
|
||||
MESSAGES_DIR = SCRIPT_DIR / "src" / "i18n" / "messages"
|
||||
SOURCE_FILE = MESSAGES_DIR / "en.json"
|
||||
TRANSLATION_FILE = MESSAGES_DIR / "cs.json"
|
||||
|
||||
# Keys that should NOT be translated (technical terms, proper names, etc.)
|
||||
UNTRANSLATABLE_KEYS = {
|
||||
# ICU/Plural formats
|
||||
"apiManager.modelsCount",
|
||||
# Technical/Protocol names
|
||||
"a2aDashboard.metadata",
|
||||
"a2aDashboard.ok",
|
||||
"a2aDashboard.url",
|
||||
"cliTools.baseUrlPlaceholder",
|
||||
"cliTools.platforms",
|
||||
"cliTools.toolDescriptions.claude",
|
||||
"cliTools.toolDescriptions.codex",
|
||||
"cliTools.toolDescriptions.cursor",
|
||||
"combos.roundRobin",
|
||||
"common.model",
|
||||
"docs.clientCherryStudioTitle",
|
||||
"docs.clientClaudeTitle",
|
||||
"docs.clientCursorTitle",
|
||||
"docs.github",
|
||||
"docs.protocolA2aTitle",
|
||||
"docs.protocolMcpTitle",
|
||||
"endpoint.chat",
|
||||
"endpoint.chatCompletions",
|
||||
"endpoint.cloudProxy",
|
||||
"endpoint.mcpCardTitle",
|
||||
"endpoint.rerank",
|
||||
"header.a2a",
|
||||
"header.mcp",
|
||||
"health.cpu",
|
||||
"health.latencyP50",
|
||||
"health.latencyP95",
|
||||
"health.latencyP99",
|
||||
"health.millisecondsShort",
|
||||
"health.notAvailable",
|
||||
"health.ok",
|
||||
"home.aliasLabel",
|
||||
"home.oauthLabel",
|
||||
"landing.brandName",
|
||||
"landing.flowProviderAnthropic",
|
||||
"landing.flowProviderGemini",
|
||||
"landing.flowProviderGithubCopilot",
|
||||
"landing.flowProviderOpenAI",
|
||||
"landing.flowToolClaudeCode",
|
||||
"landing.flowToolCline",
|
||||
"landing.flowToolCursor",
|
||||
"landing.flowToolOpenAICodex",
|
||||
"landing.github",
|
||||
"legal.terms",
|
||||
"legal.privacy",
|
||||
"logs.endpoint",
|
||||
"logs.proxy",
|
||||
"logs.console",
|
||||
"logs.request",
|
||||
"logs.audit",
|
||||
"media.interpolation",
|
||||
"media.upscale",
|
||||
"media.samples",
|
||||
"search.search",
|
||||
"search.searchTools",
|
||||
"search.webSearch",
|
||||
"search.fileSearch",
|
||||
"settings.theme",
|
||||
"settings.language",
|
||||
"settings.currency",
|
||||
"settings.timezone",
|
||||
"stats.requests",
|
||||
"stats.tokens",
|
||||
"stats.latency",
|
||||
"stats.errors",
|
||||
"themesPage.dark",
|
||||
"themesPage.light",
|
||||
"themesPage.system",
|
||||
"translator.translate",
|
||||
"translator.translateFrom",
|
||||
"translator.translateTo",
|
||||
"translator.detect",
|
||||
"translator.detectedLanguage",
|
||||
"usage.totalRequests",
|
||||
"usage.totalTokens",
|
||||
"usage.inputTokens",
|
||||
"usage.outputTokens",
|
||||
"usage.promptTokens",
|
||||
"usage.completionTokens",
|
||||
"usage.cacheReadTokens",
|
||||
"usage.cacheWriteTokens",
|
||||
}
|
||||
|
||||
|
||||
def print_header(msg: str) -> None:
|
||||
print(f"\n{BLUE}{'='*50}{NC}")
|
||||
print(f"{BLUE}{msg}{NC}")
|
||||
print(f"{BLUE}{'='*50}{NC}")
|
||||
|
||||
|
||||
def print_success(msg: str) -> None:
|
||||
print(f"{GREEN}✓ {msg}{NC}")
|
||||
|
||||
|
||||
def print_warning(msg: str) -> None:
|
||||
print(f"{YELLOW}⚠ {msg}{NC}")
|
||||
|
||||
|
||||
def print_error(msg: str) -> None:
|
||||
print(f"{RED}✗ {msg}{NC}")
|
||||
|
||||
|
||||
def load_json(path: Path) -> Dict:
|
||||
"""Load JSON file"""
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print_error(f"Invalid JSON in {path}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_all_keys(obj: Any, prefix: str = "") -> Set[str]:
|
||||
"""Recursively get all leaf keys from JSON object"""
|
||||
keys = set()
|
||||
if isinstance(obj, dict):
|
||||
for key, value in obj.items():
|
||||
new_prefix = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict):
|
||||
keys.update(get_all_keys(value, new_prefix))
|
||||
elif isinstance(value, list):
|
||||
# Handle arrays - check first element for structure
|
||||
if value and isinstance(value[0], dict):
|
||||
for i, item in enumerate(value):
|
||||
keys.update(get_all_keys(item, f"{new_prefix}[{i}]"))
|
||||
else:
|
||||
keys.add(new_prefix)
|
||||
else:
|
||||
keys.add(new_prefix)
|
||||
return keys
|
||||
|
||||
|
||||
def find_missing_keys(source: Dict, trans: Dict) -> Set[str]:
|
||||
"""Keys in source but not in translation"""
|
||||
source_keys = get_all_keys(source)
|
||||
trans_keys = get_all_keys(trans)
|
||||
return source_keys - trans_keys
|
||||
|
||||
|
||||
def find_extra_keys(source: Dict, trans: Dict) -> Set[str]:
|
||||
"""Keys in translation but not in source"""
|
||||
source_keys = get_all_keys(source)
|
||||
trans_keys = get_all_keys(trans)
|
||||
return trans_keys - source_keys
|
||||
|
||||
|
||||
def get_value_by_path(obj: Dict, path: str) -> Any:
|
||||
"""Get value from nested dict using dot notation"""
|
||||
keys = path.replace('[', '.').replace(']', '').split('.')
|
||||
current = obj
|
||||
for key in keys:
|
||||
if key.isdigit():
|
||||
idx = int(key)
|
||||
if isinstance(current, list) and idx < len(current):
|
||||
current = current[idx]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
if isinstance(current, dict) and key in current:
|
||||
current = current[key]
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
def find_untranslated(source: Dict, trans: Dict) -> Set[str]:
|
||||
"""Keys where source value equals translation (not translated), excluding untranslatable keys"""
|
||||
source_keys = get_all_keys(source)
|
||||
untranslated = set()
|
||||
|
||||
for key in source_keys:
|
||||
# Skip keys that are in the untranslatable list
|
||||
if key in UNTRANSLATABLE_KEYS:
|
||||
continue
|
||||
|
||||
source_val = get_value_by_path(source, key)
|
||||
trans_val = get_value_by_path(trans, key)
|
||||
|
||||
if source_val is not None and source_val == trans_val:
|
||||
untranslated.add(key)
|
||||
|
||||
return untranslated
|
||||
|
||||
|
||||
def compare_category(source: Dict, trans: Dict, category: str) -> Tuple[bool, List[str]]:
|
||||
"""Compare a specific category, return (complete, missing_keys)"""
|
||||
if category not in source:
|
||||
return False, [f"Category '{category}' not in source"]
|
||||
|
||||
if category not in trans:
|
||||
return False, [f"Category '{category}' missing in translation"]
|
||||
|
||||
source_keys = get_all_keys(source[category])
|
||||
trans_keys = get_all_keys(trans[category])
|
||||
missing = source_keys - trans_keys
|
||||
|
||||
return len(missing) == 0, list(missing)
|
||||
|
||||
|
||||
def generate_report():
|
||||
"""Generate full translation report"""
|
||||
print_header("OmniRoute Translation Report")
|
||||
print(f"Source: {SOURCE_FILE}")
|
||||
print(f"Translation: {TRANSLATION_FILE}\n")
|
||||
|
||||
source = load_json(SOURCE_FILE)
|
||||
trans = load_json(TRANSLATION_FILE)
|
||||
|
||||
# Count keys
|
||||
source_count = len(get_all_keys(source))
|
||||
trans_count = len(get_all_keys(trans))
|
||||
|
||||
print(f"{BLUE}Key Statistics:{NC}")
|
||||
print(f" Source keys: {source_count}")
|
||||
print(f" Translation keys: {trans_count}\n")
|
||||
|
||||
# Missing keys
|
||||
print_header("Missing Translations")
|
||||
missing = find_missing_keys(source, trans)
|
||||
if missing:
|
||||
print(f"{RED}Found {len(missing)} missing keys:{NC}")
|
||||
for key in sorted(missing)[:50]: # Limit output
|
||||
print(f" - {key}")
|
||||
if len(missing) > 50:
|
||||
print(f" ... and {len(missing) - 50} more")
|
||||
else:
|
||||
print_success("No missing translations!")
|
||||
|
||||
# Extra keys
|
||||
print_header("Extra Keys")
|
||||
extra = find_extra_keys(source, trans)
|
||||
if extra:
|
||||
print(f"{YELLOW}Found {len(extra)} extra keys:{NC}")
|
||||
for key in sorted(extra)[:50]:
|
||||
print(f" - {key}")
|
||||
else:
|
||||
print_success("No extra keys!")
|
||||
|
||||
# Untranslated
|
||||
print_header("Untranslated Keys (same as source)")
|
||||
untranslated = find_untranslated(source, trans)
|
||||
if untranslated:
|
||||
print(f"{YELLOW}Found {len(untranslated)} untranslated keys:{NC}")
|
||||
for key in sorted(untranslated)[:50]:
|
||||
print(f" - {key}")
|
||||
if len(untranslated) > 50:
|
||||
print(f" ... and {len(untranslated) - 50} more")
|
||||
else:
|
||||
print_success("All keys appear to be translated!")
|
||||
|
||||
# Per-category status
|
||||
print_header("Per-Category Status")
|
||||
for category in sorted(source.keys()):
|
||||
complete, missing = compare_category(source, trans, category)
|
||||
if complete:
|
||||
print_success(f"{category} - complete")
|
||||
else:
|
||||
print_error(f"{category} - missing {len(missing)} keys")
|
||||
|
||||
# Summary
|
||||
print_header("Summary")
|
||||
if not missing and not extra and not untranslated:
|
||||
print(f"{GREEN}🎉 Translation is fully synchronized!{NC}")
|
||||
return 0
|
||||
else:
|
||||
print(f"{RED}Translation needs attention:{NC}")
|
||||
print(f" - Missing: {len(missing)}")
|
||||
print(f" - Extra: {len(extra)}")
|
||||
print(f" - Untranslated: {len(untranslated)}")
|
||||
return 1
|
||||
|
||||
|
||||
def quick_check() -> int:
|
||||
"""Quick check - just show counts"""
|
||||
source = load_json(SOURCE_FILE)
|
||||
trans = load_json(TRANSLATION_FILE)
|
||||
|
||||
missing = find_missing_keys(source, trans)
|
||||
untranslated = find_untranslated(source, trans)
|
||||
|
||||
print(f"Missing: {len(missing)}")
|
||||
print(f"Untranslated: {len(untranslated)}")
|
||||
|
||||
return 0 if not missing and not untranslated else 1
|
||||
|
||||
|
||||
def show_diff(category: str) -> int:
|
||||
"""Show detailed diff for a category"""
|
||||
source = load_json(SOURCE_FILE)
|
||||
trans = load_json(TRANSLATION_FILE)
|
||||
|
||||
if category not in source:
|
||||
print_error(f"Category '{category}' not found in source")
|
||||
print("Available categories:")
|
||||
for cat in sorted(source.keys()):
|
||||
print(f" - {cat}")
|
||||
return 1
|
||||
|
||||
print_header(f"Diff for category: {category}")
|
||||
|
||||
print(f"{BLUE}{'Key':<30} | {'Source':<25} | {'Translation':<25}{NC}")
|
||||
print("-" * 85)
|
||||
|
||||
source_keys = get_all_keys(source[category])
|
||||
|
||||
for key in sorted(source_keys):
|
||||
source_val = get_value_by_path(source[category], key)
|
||||
trans_val = get_value_by_path(trans.get(category, {}), key)
|
||||
|
||||
# Truncate long values
|
||||
source_str = str(source_val)[:25] if source_val else "(null)"
|
||||
trans_str = str(trans_val)[:25] if trans_val else "(missing)"
|
||||
|
||||
if source_val == trans_val:
|
||||
status = f"{YELLOW}(same){NC}"
|
||||
elif trans_val is None:
|
||||
status = f"{RED}(missing){NC}"
|
||||
else:
|
||||
status = f"{GREEN}(ok){NC}"
|
||||
|
||||
print(f"{key:<30} | {source_str:<25} | {trans_str:<25} {status}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def export_csv(output_file: str) -> int:
|
||||
"""Export to CSV"""
|
||||
source = load_json(SOURCE_FILE)
|
||||
trans = load_json(TRANSLATION_FILE)
|
||||
|
||||
print_header(f"Exporting to CSV: {output_file}")
|
||||
|
||||
source_keys = get_all_keys(source)
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("key,source_value,translation_value,status\n")
|
||||
|
||||
for key in sorted(source_keys):
|
||||
source_val = get_value_by_path(source, key)
|
||||
trans_val = get_value_by_path(trans, key)
|
||||
|
||||
# Escape commas
|
||||
source_str = str(source_val).replace(',', ';')
|
||||
trans_str = str(trans_val).replace(',', ';') if trans_val else ""
|
||||
|
||||
if trans_val is None:
|
||||
status = "MISSING"
|
||||
elif source_val == trans_val:
|
||||
status = "UNTRANSLATED"
|
||||
else:
|
||||
status = "OK"
|
||||
|
||||
f.write(f'"{key}","{source_str}","{trans_str}",{status}\n')
|
||||
|
||||
print_success(f"Exported to {output_file}")
|
||||
return 0
|
||||
|
||||
|
||||
def export_markdown(output_file: str) -> int:
|
||||
"""Export all keys to separate Markdown files - translated and untranslated"""
|
||||
source = load_json(SOURCE_FILE)
|
||||
trans = load_json(TRANSLATION_FILE)
|
||||
|
||||
print_header(f"Exporting to Markdown: {output_file}")
|
||||
|
||||
source_keys = get_all_keys(source)
|
||||
missing = find_missing_keys(source, trans)
|
||||
untranslated = find_untranslated(source, trans)
|
||||
|
||||
# Separate translated and untranslated
|
||||
translated_keys = []
|
||||
untranslated_sorted = sorted(untranslated)
|
||||
|
||||
for key in sorted(source_keys):
|
||||
if key not in missing and key not in untranslated:
|
||||
translated_keys.append(key)
|
||||
|
||||
translated_count = len(translated_keys)
|
||||
untranslated_count = len(untranslated_sorted)
|
||||
|
||||
# Export untranslated (main output file)
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("# Nepřeložené klíče (Untranslated Keys)\n\n")
|
||||
f.write(f"Zdroj: `{SOURCE_FILE.name}` | Překlad: `{TRANSLATION_FILE.name}`\n\n")
|
||||
|
||||
f.write(f"**Celkem: {untranslated_count} nepreložených klíčů**\n\n")
|
||||
|
||||
f.write("| # | Klíč (Key) | Originál | Nepřeloženo |\n")
|
||||
f.write("|---|------------|----------|------------|\n")
|
||||
|
||||
for i, key in enumerate(untranslated_sorted, 1):
|
||||
source_val = get_value_by_path(source, key)
|
||||
trans_val = get_value_by_path(trans, key)
|
||||
|
||||
source_str = str(source_val).replace('|', '\\|')[:60]
|
||||
trans_str = str(trans_val).replace('|', '\\|')[:60]
|
||||
|
||||
f.write(f"| {i} | `{key}` | {source_str} | {trans_str} |\n")
|
||||
|
||||
f.write("\n## Shrnutí (Summary)\n\n")
|
||||
f.write(f"- Celkem klíčů: {len(source_keys)}\n")
|
||||
f.write(f"- Chybějících: {len(missing)}\n")
|
||||
f.write(f"- Nepřeložených: {untranslated_count}\n")
|
||||
f.write(f"- Přeložených: {translated_count}\n")
|
||||
|
||||
# Export translated to separate file
|
||||
translated_file = output_file.replace('.md', '_translated.md')
|
||||
with open(translated_file, 'w', encoding='utf-8') as f:
|
||||
f.write("# Přeložené klíče (Translated Keys)\n\n")
|
||||
f.write(f"Zdroj: `{SOURCE_FILE.name}` | Překlad: `{TRANSLATION_FILE.name}`\n\n")
|
||||
|
||||
f.write(f"**Celkem: {translated_count} přeložených klíčů**\n\n")
|
||||
|
||||
f.write("| # | Klíč (Key) | Originál | Překlad |\n")
|
||||
f.write("|---|------------|----------|---------|\n")
|
||||
|
||||
for i, key in enumerate(translated_keys, 1):
|
||||
source_val = get_value_by_path(source, key)
|
||||
trans_val = get_value_by_path(trans, key)
|
||||
|
||||
source_str = str(source_val).replace('|', '\\|')[:40]
|
||||
trans_str = str(trans_val).replace('|', '\\|')[:40]
|
||||
|
||||
f.write(f"| {i} | `{key}` | {source_str} | {trans_str} |\n")
|
||||
|
||||
print_success(f"Exported: {output_file} ({untranslated_count} keys)")
|
||||
print_success(f"Exported: {translated_file} ({translated_count} keys)")
|
||||
return 0
|
||||
|
||||
|
||||
def usage():
|
||||
print("""
|
||||
OmniRoute i18n Translation Validator
|
||||
|
||||
Usage: validate_translation.py [command] [options]
|
||||
|
||||
Commands:
|
||||
(default) Generate full report
|
||||
quick Quick check - just show counts
|
||||
diff <category> Show detailed diff for a category
|
||||
csv [file] Export to CSV (default: translation_report.csv)
|
||||
md [file] Export to Markdown (default: translation_report.md)
|
||||
|
||||
Examples:
|
||||
python validate_translation.py # Full report
|
||||
python validate_translation.py quick # Quick status check
|
||||
python validate_translation.py diff common # Diff common category
|
||||
python validate_translation.py csv # Export to CSV
|
||||
python validate_translation.py md # Export to Markdown
|
||||
""")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
return generate_report()
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "quick":
|
||||
return quick_check()
|
||||
elif cmd == "diff":
|
||||
if len(sys.argv) < 3:
|
||||
print_error("Please specify category")
|
||||
usage()
|
||||
return 1
|
||||
return show_diff(sys.argv[2])
|
||||
elif cmd == "csv":
|
||||
output = sys.argv[2] if len(sys.argv) > 2 else "translation_report.csv"
|
||||
return export_csv(output)
|
||||
elif cmd == "md":
|
||||
output = sys.argv[2] if len(sys.argv) > 2 else "translation_report.md"
|
||||
return export_markdown(output)
|
||||
elif cmd in ("help", "--help", "-h"):
|
||||
usage()
|
||||
return 0
|
||||
else:
|
||||
print_error(f"Unknown command: {cmd}")
|
||||
usage()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user