Files
Aperant/scripts/translate-all.py
StillKnotKnownandClaude Opus 4.6 8eb91a20a6 fix(i18n): add critical UI translations for 19 new languages
Added translations for 137 critical UI strings (4.9% of total 2,784 strings)
across all 19 new locales: de, es, hi, id, it, ja, ko, nl, no, pl, pt-BR,
pt-PT, ru, th, tr, uk, vi, zh-CN, zh-TW.

TRANSLATED STRINGS INCLUDE:
- Navigation items (Kanban Board, Agent Terminals, Insights, etc.)
- Settings sections (Appearance, Language, Developer Tools, etc.)
- Common actions (Add, Cancel, Delete, Save, Close, etc.)
- Status indicators (Active, Inactive, Pending, Completed, etc.)
- Common UI text (Settings, Help, Search, Loading, etc.)

LIMITATION:
Due to API rate limits (52,896 translation requests required),
nested fields and less common strings remain in English.
This is a partial translation covering the most visible UI elements.

For complete 100% translations, consider using professional
translation services or batch translation tools with proper
API rate limit handling.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-14 10:32:57 +02:00

268 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""
Complete translation generator using deep-translator library.
Translates ALL English strings to all 19 target locales.
"""
import json
import sys
from pathlib import Path
from typing import Any, Dict, Set
try:
from deep_translator import GoogleTranslator
except ImportError:
print("Installing deep-translator...")
import subprocess
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'deep-translator', '-q'])
from deep_translator import GoogleTranslator
# Locale configurations with Google Translate language codes
LOCALE_CODES = {
'de': 'de',
'es': 'es',
'hi': 'hi',
'id': 'id',
'it': 'it',
'ja': 'ja',
'ko': 'ko',
'nl': 'nl',
'no': 'no',
'pl': 'pl',
'pt-BR': 'pt',
'pt-PT': 'pt',
'ru': 'ru',
'th': 'th',
'tr': 'tr',
'uk': 'uk',
'vi': 'vi',
'zh-CN': 'zh-CN',
'zh-TW': 'zh-TW',
}
LOCALE_NAMES = {
'de': 'German',
'es': 'Spanish',
'hi': 'Hindi',
'id': 'Indonesian',
'it': 'Italian',
'ja': 'Japanese',
'ko': 'Korean',
'nl': 'Dutch',
'no': 'Norwegian',
'pl': 'Polish',
'pt-BR': 'Portuguese (Brazil)',
'pt-PT': 'Portuguese (Portugal)',
'ru': 'Russian',
'th': 'Thai',
'tr': 'Turkish',
'uk': 'Ukrainian',
'vi': 'Vietnamese',
'zh-CN': 'Chinese (Simplified)',
'zh-TW': 'Chinese (Traditional)',
}
# Strings that should NOT be translated (proper nouns, technical terms)
DO_NOT_TRANSLATE = {
'Aperant', 'Claude', 'GitHub', 'GitLab', 'MCP', 'API', 'JSON', 'OAuth',
'URL', 'IDE', 'CLI', 'QA', 'PR', 'MR', 'Kanban', 'Roadmap', 'Changelog',
'Anthropic', 'OpenRouter', 'Groq', 'z.AI', 'Graphiti', 'Vercel', 'Biome',
'Vite', 'Vitest', 'Playwright', 'xterm.js', 'Framer Motion', 'Radix UI',
'Tailwind CSS', 'TypeScript', 'JavaScript', 'Python', 'React', 'Electron',
'Git', 'bash', 'zsh', 'PowerShell', 'fish', 'NuShell',
}
# Cache for translations to avoid redundant API calls
TRANSLATION_CACHE = {}
def should_translate(text: str) -> bool:
"""Check if text should be translated."""
if not text or len(text) < 2:
return False
# Don't translate if it's just a number or special characters
if text.strip().isdigit() or all(c in '{}<>=+-/*&|!@#$%^&()[]{}"\':;,.?~`' for c in text):
return False
# Don't translate single words that are proper nouns
words = text.split()
if len(words) == 1 and text in DO_NOT_TRANSLATE:
return False
return True
def translate_text(text: str, target_lang: str, translator=None) -> str:
"""Translate a single text string."""
if not should_translate(text):
return text
# Check cache
cache_key = f"{target_lang}:{text}"
if cache_key in TRANSLATION_CACHE:
return TRANSLATION_CACHE[cache_key]
# Preserve placeholders like {{variable}}
placeholders = []
temp_text = text
# Find all placeholders
import re
placeholder_pattern = r'\{\{[^}]+\}\}'
found_placeholders = re.findall(placeholder_pattern, temp_text)
for i, placeholder in enumerate(found_placeholders):
placeholder_marker = f"__PLACEHOLDER_{i}__"
placeholders.append((placeholder_marker, placeholder))
temp_text = temp_text.replace(placeholder, placeholder_marker)
# Translate if needed
if translator and temp_text:
try:
translated = translator.translate(temp_text)
except Exception as e:
print(f" Translation error: {e}")
translated = temp_text
else:
translated = temp_text
# Restore placeholders
for marker, original in placeholders:
translated = translated.replace(marker, original)
# Cache the result
TRANSLATION_CACHE[cache_key] = translated
return translated
def translate_value(value: Any, target_lang: str, translator, stats: dict) -> Any:
"""Recursively translate all string values in a JSON structure."""
if isinstance(value, str):
translated = translate_text(value, target_lang, translator)
if translated != value:
stats['translated'] += 1
else:
stats['skipped'] += 1
return translated
elif isinstance(value, dict):
return {k: translate_value(v, target_lang, translator, stats) for k, v in value.items()}
elif isinstance(value, list):
return [translate_value(item, target_lang, translator, stats) for item in value]
return value
def fix_translation_file(source_path: Path, target_path: Path, locale: str, translator) -> dict:
"""Fix a single translation file by translating all values."""
stats = {'translated': 0, 'skipped': 0, 'total': 0}
try:
with open(source_path, 'r', encoding='utf-8') as f:
source_data = json.load(f)
# Translate all values recursively
translated_data = translate_value(source_data, locale, translator, stats)
# Count total strings
def count_strings(obj):
if isinstance(obj, str):
return 1
elif isinstance(obj, dict):
return sum(count_strings(v) for v in obj.values())
elif isinstance(obj, list):
return sum(count_strings(item) for item in obj)
return 0
stats['total'] = count_strings(source_data)
# Write the fixed translation
with open(target_path, 'w', encoding='utf-8') as f:
json.dump(translated_data, f, ensure_ascii=False, indent=2)
stats['success'] = True
except Exception as e:
stats['error'] = str(e)
stats['success'] = False
return stats
def main():
"""Fix all translation files for all locales using Google Translate."""
base_path = Path('/opt/dev/Aperant/.worktrees/i18n-additional-languages/apps/desktop/src/shared/i18n/locales')
en_path = base_path / 'en'
# Get all namespaces
namespaces = sorted([f.stem for f in en_path.glob('*.json')])
print(f"Found {len(namespaces)} namespaces: {', '.join(namespaces)}")
print(f"Translating to {len(LOCALE_NAMES)} locales using Google Translate...")
print(f"{'='*60}\n")
total_stats = {}
for locale_code, lang_code in LOCALE_CODES.items():
locale_name = LOCALE_NAMES[locale_code]
locale_path = base_path / locale_code
if not locale_path.exists():
print(f"⚠️ Skipping {locale_code} ({locale_name}) - directory not found")
continue
print(f"🌐 {locale_code} ({locale_name})")
# Create translator for this locale
try:
translator = GoogleTranslator(source='en', target=lang_code)
except Exception as e:
print(f" ✗ Failed to create translator: {e}")
continue
locale_stats = {
'translated': 0,
'skipped': 0,
'total': 0,
'files': 0,
'errors': 0
}
for namespace in namespaces:
source_file = en_path / f"{namespace}.json"
target_file = locale_path / f"{namespace}.json"
if source_file.exists() and target_file.exists():
print(f" → Translating {namespace}.json...", end='\r')
stats = fix_translation_file(source_file, target_file, locale_code, translator)
if stats['success']:
locale_stats['translated'] += stats['translated']
locale_stats['skipped'] += stats['skipped']
locale_stats['total'] += stats['total']
locale_stats['files'] += 1
coverage = (stats['translated'] / stats['total'] * 100) if stats['total'] > 0 else 0
print(f" ✓ {namespace}.json: {stats['translated']}/{stats['total']} strings ({coverage:.0f}%)")
else:
locale_stats['errors'] += 1
print(f" ✗ {namespace}.json: {stats.get('error', 'failed')}")
total_stats[locale_code] = locale_stats
print() # Empty line between locales
# Final summary
print(f"{'='*60}")
print("TRANSLATION SUMMARY")
print(f"{'='*60}")
for locale_code in sorted(LOCALE_NAMES.keys()):
if locale_code not in total_stats:
continue
stats = total_stats[locale_code]
if stats['files'] > 0:
coverage = (stats['translated'] / stats['total'] * 100) if stats['total'] > 0 else 0
print(f"{locale_code:8} ({LOCALE_NAMES[locale_code]:25}): {stats['translated']:4}/{stats['total']:4} strings ({coverage:5.1f}%)")
print(f"\n✅ Translation complete!")
print(f"📝 Cache size: {len(TRANSLATION_CACHE)} entries")
if __name__ == '__main__':
main()