mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
- Added a new translation management system to automate synchronization, validation, and translation of internationalization files. - Introduced scripts for running translation operations, including checking status, synchronizing keys, and auto-translating strings. - Updated package.json with new translation-related commands for easier access. - Enhanced localization files across multiple languages with new keys and improved translations. - Integrated download functionality for share files in the UI, allowing users to download multiple files seamlessly. - Refactored components to support new download features and improved user experience.
342 lines
12 KiB
Python
342 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Script para traduzir automaticamente strings marcadas com [TO_TRANSLATE]
|
|
usando Google Translate gratuito.
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List, Tuple, Optional
|
|
import argparse
|
|
import sys
|
|
|
|
# Mapeamento de códigos de idioma dos arquivos para códigos do Google Translate
|
|
LANGUAGE_MAPPING = {
|
|
'pt-BR.json': 'pt', # Português (Brasil) -> Português
|
|
'es-ES.json': 'es', # Espanhol (Espanha) -> Espanhol
|
|
'fr-FR.json': 'fr', # Francês (França) -> Francês
|
|
'de-DE.json': 'de', # Alemão -> Alemão
|
|
'it-IT.json': 'it', # Italiano -> Italiano
|
|
'ru-RU.json': 'ru', # Russo -> Russo
|
|
'ja-JP.json': 'ja', # Japonês -> Japonês
|
|
'ko-KR.json': 'ko', # Coreano -> Coreano
|
|
'zh-CN.json': 'zh-cn', # Chinês (Simplificado) -> Chinês Simplificado
|
|
'ar-SA.json': 'ar', # Árabe -> Árabe
|
|
'hi-IN.json': 'hi', # Hindi -> Hindi
|
|
'nl-NL.json': 'nl', # Holandês -> Holandês
|
|
'tr-TR.json': 'tr', # Turco -> Turco
|
|
'pl-PL.json': 'pl', # Polonês -> Polonês
|
|
}
|
|
|
|
# Prefixo para identificar strings não traduzidas
|
|
TO_TRANSLATE_PREFIX = '[TO_TRANSLATE] '
|
|
|
|
|
|
def load_json_file(file_path: Path) -> Dict[str, Any]:
|
|
"""Carrega um arquivo JSON."""
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
except Exception as e:
|
|
print(f"❌ Erro ao carregar {file_path}: {e}")
|
|
return {}
|
|
|
|
|
|
def save_json_file(file_path: Path, data: Dict[str, Any], indent: int = 2) -> bool:
|
|
"""Salva um arquivo JSON com formatação consistente."""
|
|
try:
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=indent, separators=(',', ': '))
|
|
f.write('\n') # Adiciona nova linha no final
|
|
return True
|
|
except Exception as e:
|
|
print(f"❌ Erro ao salvar {file_path}: {e}")
|
|
return False
|
|
|
|
|
|
def get_nested_value(data: Dict[str, Any], key_path: str) -> Any:
|
|
"""Obtém um valor aninhado usando uma chave com pontos como separador."""
|
|
keys = key_path.split('.')
|
|
current = data
|
|
|
|
for key in keys:
|
|
if isinstance(current, dict) and key in current:
|
|
current = current[key]
|
|
else:
|
|
return None
|
|
|
|
return current
|
|
|
|
|
|
def set_nested_value(data: Dict[str, Any], key_path: str, value: Any) -> None:
|
|
"""Define um valor aninhado usando uma chave com pontos como separador."""
|
|
keys = key_path.split('.')
|
|
current = data
|
|
|
|
# Navega até o penúltimo nível, criando dicionários conforme necessário
|
|
for key in keys[:-1]:
|
|
if key not in current:
|
|
current[key] = {}
|
|
elif not isinstance(current[key], dict):
|
|
current[key] = {}
|
|
current = current[key]
|
|
|
|
# Define o valor no último nível
|
|
current[keys[-1]] = value
|
|
|
|
|
|
def find_untranslated_strings(data: Dict[str, Any], prefix: str = '') -> List[Tuple[str, str]]:
|
|
"""Encontra todas as strings marcadas com [TO_TRANSLATE] recursivamente."""
|
|
untranslated = []
|
|
|
|
for key, value in data.items():
|
|
current_key = f"{prefix}.{key}" if prefix else key
|
|
|
|
if isinstance(value, str) and value.startswith(TO_TRANSLATE_PREFIX):
|
|
# Remove o prefixo para obter o texto original
|
|
original_text = value[len(TO_TRANSLATE_PREFIX):].strip()
|
|
untranslated.append((current_key, original_text))
|
|
elif isinstance(value, dict):
|
|
untranslated.extend(find_untranslated_strings(value, current_key))
|
|
|
|
return untranslated
|
|
|
|
|
|
def install_googletrans():
|
|
"""Instala a biblioteca googletrans se não estiver disponível."""
|
|
try:
|
|
import googletrans
|
|
return True
|
|
except ImportError:
|
|
print("📦 Biblioteca 'googletrans' não encontrada. Tentando instalar...")
|
|
import subprocess
|
|
try:
|
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "googletrans==4.0.0rc1"])
|
|
print("✅ googletrans instalada com sucesso!")
|
|
return True
|
|
except subprocess.CalledProcessError:
|
|
print("❌ Falha ao instalar googletrans. Instale manualmente com:")
|
|
print("pip install googletrans==4.0.0rc1")
|
|
return False
|
|
|
|
|
|
def translate_text(text: str, target_language: str, max_retries: int = 3) -> Optional[str]:
|
|
"""Traduz um texto usando Google Translate gratuito."""
|
|
try:
|
|
from googletrans import Translator
|
|
|
|
translator = Translator()
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
# Traduz do inglês para o idioma alvo
|
|
result = translator.translate(text, src='en', dest=target_language)
|
|
|
|
if result and result.text:
|
|
return result.text.strip()
|
|
|
|
except Exception as e:
|
|
if attempt < max_retries - 1:
|
|
print(f" ⚠️ Tentativa {attempt + 1} falhou: {str(e)[:50]}... Reentando em 2s...")
|
|
time.sleep(2)
|
|
else:
|
|
print(f" ❌ Falha após {max_retries} tentativas: {str(e)[:50]}...")
|
|
|
|
return None
|
|
|
|
except ImportError:
|
|
print("❌ Biblioteca googletrans não disponível")
|
|
return None
|
|
|
|
|
|
def translate_file(file_path: Path, target_language: str, dry_run: bool = False,
|
|
delay_between_requests: float = 1.0) -> Tuple[int, int, int]:
|
|
"""
|
|
Traduz todas as strings [TO_TRANSLATE] em um arquivo.
|
|
Retorna: (total_found, successful_translations, failed_translations)
|
|
"""
|
|
print(f"🔍 Processando: {file_path.name}")
|
|
|
|
# Carrega o arquivo
|
|
data = load_json_file(file_path)
|
|
if not data:
|
|
return 0, 0, 0
|
|
|
|
# Encontra strings não traduzidas
|
|
untranslated_strings = find_untranslated_strings(data)
|
|
|
|
if not untranslated_strings:
|
|
print(f" ✅ Nenhuma string para traduzir")
|
|
return 0, 0, 0
|
|
|
|
print(f" 📝 Encontradas {len(untranslated_strings)} strings para traduzir")
|
|
|
|
if dry_run:
|
|
print(f" 🔍 [DRY RUN] Strings que seriam traduzidas:")
|
|
for key, text in untranslated_strings[:3]:
|
|
print(f" - {key}: \"{text[:50]}{'...' if len(text) > 50 else ''}\"")
|
|
if len(untranslated_strings) > 3:
|
|
print(f" ... e mais {len(untranslated_strings) - 3}")
|
|
return len(untranslated_strings), 0, 0
|
|
|
|
# Traduz cada string
|
|
successful = 0
|
|
failed = 0
|
|
updated_data = data.copy()
|
|
|
|
for i, (key_path, original_text) in enumerate(untranslated_strings, 1):
|
|
print(f" 📍 ({i}/{len(untranslated_strings)}) Traduzindo: {key_path}")
|
|
|
|
# Traduz o texto
|
|
translated_text = translate_text(original_text, target_language)
|
|
|
|
if translated_text and translated_text != original_text:
|
|
# Atualiza no dicionário
|
|
set_nested_value(updated_data, key_path, translated_text)
|
|
successful += 1
|
|
print(f" ✅ \"{original_text[:30]}...\" → \"{translated_text[:30]}...\"")
|
|
else:
|
|
failed += 1
|
|
print(f" ❌ Falha na tradução")
|
|
|
|
# Delay entre requisições para evitar rate limiting
|
|
if i < len(untranslated_strings): # Não espera após a última
|
|
time.sleep(delay_between_requests)
|
|
|
|
# Salva o arquivo atualizado
|
|
if successful > 0:
|
|
if save_json_file(file_path, updated_data):
|
|
print(f" 💾 Arquivo salvo com {successful} traduções")
|
|
else:
|
|
print(f" ❌ Erro ao salvar arquivo")
|
|
failed += successful # Conta como falha se não conseguiu salvar
|
|
successful = 0
|
|
|
|
return len(untranslated_strings), successful, failed
|
|
|
|
|
|
def translate_all_files(messages_dir: Path, delay_between_requests: float = 1.0,
|
|
dry_run: bool = False, skip_languages: List[str] = None) -> None:
|
|
"""Traduz todos os arquivos de idioma que têm strings [TO_TRANSLATE]."""
|
|
|
|
if not install_googletrans():
|
|
return
|
|
|
|
skip_languages = skip_languages or []
|
|
|
|
# Encontra arquivos JSON de idioma
|
|
language_files = []
|
|
for file_name, lang_code in LANGUAGE_MAPPING.items():
|
|
file_path = messages_dir / file_name
|
|
if file_path.exists() and file_name not in skip_languages:
|
|
language_files.append((file_path, lang_code))
|
|
|
|
if not language_files:
|
|
print("❌ Nenhum arquivo de idioma encontrado")
|
|
return
|
|
|
|
print(f"🌍 Traduzindo {len(language_files)} idiomas...")
|
|
print(f"⏱️ Delay entre requisições: {delay_between_requests}s")
|
|
if dry_run:
|
|
print("🔍 MODO DRY RUN - Nenhuma alteração será feita")
|
|
print("-" * 60)
|
|
|
|
total_found = 0
|
|
total_successful = 0
|
|
total_failed = 0
|
|
|
|
for i, (file_path, lang_code) in enumerate(language_files, 1):
|
|
print(f"\n[{i}/{len(language_files)}] 🌐 Idioma: {lang_code.upper()}")
|
|
|
|
found, successful, failed = translate_file(
|
|
file_path, lang_code, dry_run, delay_between_requests
|
|
)
|
|
|
|
total_found += found
|
|
total_successful += successful
|
|
total_failed += failed
|
|
|
|
# Pausa entre arquivos (exceto o último)
|
|
if i < len(language_files) and not dry_run:
|
|
print(f" ⏸️ Pausando {delay_between_requests * 2}s antes do próximo idioma...")
|
|
time.sleep(delay_between_requests * 2)
|
|
|
|
# Sumário final
|
|
print("\n" + "=" * 60)
|
|
print("📊 SUMÁRIO FINAL")
|
|
print("=" * 60)
|
|
|
|
if dry_run:
|
|
print(f"🔍 MODO DRY RUN:")
|
|
print(f" • {total_found} strings seriam traduzidas")
|
|
else:
|
|
print(f"✅ Traduções realizadas:")
|
|
print(f" • {total_successful} sucessos")
|
|
print(f" • {total_failed} falhas")
|
|
print(f" • {total_found} total processadas")
|
|
|
|
if total_successful > 0:
|
|
success_rate = (total_successful / total_found) * 100
|
|
print(f" • Taxa de sucesso: {success_rate:.1f}%")
|
|
|
|
print("\n💡 DICAS:")
|
|
print("• Execute 'python3 check_translations.py' para verificar o resultado")
|
|
print("• Strings que falharam na tradução mantêm o prefixo [TO_TRANSLATE]")
|
|
print("• Considere revisar as traduções automáticas para garantir qualidade")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Traduz automaticamente strings marcadas com [TO_TRANSLATE]'
|
|
)
|
|
parser.add_argument(
|
|
'--messages-dir',
|
|
type=Path,
|
|
default=Path(__file__).parent,
|
|
help='Diretório contendo os arquivos de mensagem (padrão: diretório atual)'
|
|
)
|
|
parser.add_argument(
|
|
'--dry-run',
|
|
action='store_true',
|
|
help='Apenas mostra o que seria traduzido, sem fazer alterações'
|
|
)
|
|
parser.add_argument(
|
|
'--delay',
|
|
type=float,
|
|
default=1.0,
|
|
help='Delay em segundos entre requisições de tradução (padrão: 1.0)'
|
|
)
|
|
parser.add_argument(
|
|
'--skip-languages',
|
|
nargs='*',
|
|
default=[],
|
|
help='Lista de idiomas para pular (ex: pt-BR.json fr-FR.json)'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.messages_dir.exists():
|
|
print(f"❌ Diretório não encontrado: {args.messages_dir}")
|
|
return 1
|
|
|
|
print(f"📁 Diretório: {args.messages_dir}")
|
|
print(f"🔍 Dry run: {args.dry_run}")
|
|
print(f"⏱️ Delay: {args.delay}s")
|
|
if args.skip_languages:
|
|
print(f"⏭️ Ignorando: {', '.join(args.skip_languages)}")
|
|
print("-" * 60)
|
|
|
|
translate_all_files(
|
|
messages_dir=args.messages_dir,
|
|
delay_between_requests=args.delay,
|
|
dry_run=args.dry_run,
|
|
skip_languages=args.skip_languages
|
|
)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
exit(main()) |