#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ batch_process_library.py — Processa uma biblioteca inteira de traduções .po WordPress. Executa em sequência os passos obrigatórios em TODOS os ficheiros *-pt_PT.po: 1. fix_malformed — Corrige sintaxe .po mal formada 2. fix_ptbr — Corrige PT-BR → PT-PT (saltado com --deepl, DeepL gera PT-PT nativo) 3. translate_missing — Traduz strings em falta via DeepL PT-PT (padrão) ou LibreTranslate No final compila todos os .mo via polib e gera relatório. Uso: python3 batch_process_library.py /path/to/library python3 batch_process_library.py /path/to/library --skip-translate python3 batch_process_library.py /path/to/library --only-ptbr python3 batch_process_library.py /path/to/library --dry-run python3 batch_process_library.py /path/to/library --no-deepl (usar LibreTranslate) Exemplos: python3 batch_process_library.py /media/ealmeida/Dados/Dev/WordPress/Tradução-Plugins-PT-PT python3 batch_process_library.py /media/ealmeida/Dados/Dev/WordPress/Tradução-Plugins-PT-PT --skip-translate Author: Descomplicar® Version: 2.0.0 — DeepL PT-PT """ import sys import os import subprocess import time import argparse from pathlib import Path from datetime import datetime SCRIPT_DIR = Path(__file__).parent FIX_MALFORMED = SCRIPT_DIR / 'fix_malformed.py' FIX_PTBR = SCRIPT_DIR / 'fix_ptbr.py' TRANSLATE_MISSING = SCRIPT_DIR / 'translate_missing.py' def find_po_files(library_path: Path) -> list[Path]: """Encontra todos os ficheiros *-pt_PT.po na biblioteca (maxdepth 2).""" po_files = [] for po in sorted(library_path.rglob('*-pt_PT.po')): # Ignorar backups if '.bak' in po.name: continue # Máximo 2 níveis de profundidade rel = po.relative_to(library_path) if len(rel.parts) <= 2: po_files.append(po) return po_files def run_script(script: Path, files: list[Path], extra_args: list[str] = None) -> int: """Executa um script Python com lista de ficheiros.""" if not files: return 0 cmd = ['python3', str(script)] + [str(f) for f in files] if extra_args: cmd += extra_args result = subprocess.run(cmd, capture_output=False) return result.returncode def compile_mo(po_file: Path) -> bool: """Compila um .po para .mo via polib (mais permissivo que msgfmt).""" mo_file = po_file.with_suffix('.mo') try: import polib po = polib.pofile(str(po_file), encoding='utf-8') po.save_as_mofile(str(mo_file)) return True except Exception: # Fallback: msgfmt r = subprocess.run( ['msgfmt', str(po_file), '-o', str(mo_file)], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) return r.returncode == 0 def count_empty_strings(po_file: Path) -> tuple[int, int]: """Conta strings totais e vazias num .po. Devolve (total, vazias).""" try: with open(po_file, 'r', encoding='utf-8', errors='replace') as f: content = f.read() total = content.count('\nmsgid "') + (1 if content.startswith('msgid "') else 0) # Não contar o header (msgid "") empty = content.count('\nmsgstr ""\n') return max(0, total - 1), empty except Exception: return 0, 0 def main(): parser = argparse.ArgumentParser( description='Processa biblioteca de traduções .po WordPress para PT-PT' ) parser.add_argument('library', help='Caminho para a pasta com os plugins') parser.add_argument('--skip-translate', action='store_true', help='Saltar tradução de strings em falta (só correcções)') parser.add_argument('--only-ptbr', action='store_true', help='Executar apenas correcções PT-BR (sem tradução)') parser.add_argument('--dry-run', action='store_true', help='Simular sem gravar alterações') parser.add_argument('--no-deepl', action='store_true', help='Usar LibreTranslate em vez de DeepL (legado)') args = parser.parse_args() library = Path(args.library) if not library.exists(): print(f"ERRO: pasta não encontrada: {library}", file=sys.stderr) sys.exit(1) start_time = time.time() timestamp = datetime.now().strftime('%d-%m-%Y %H:%M') print(f"\n{'='*65}") print(f" Batch Process Library — PT-PT WordPress Translations") print(f" {timestamp}") print(f"{'='*65}") print(f" Biblioteca: {library}") # Encontrar todos os .po po_files = find_po_files(library) if not po_files: print(f"\nNenhum ficheiro *-pt_PT.po encontrado em {library}") sys.exit(0) print(f" Ficheiros .po encontrados: {len(po_files)}") # --- PASSO 1: Corrigir malformações --- if not args.only_ptbr: print(f"\n{'─'*65}") print(f" PASSO 1/3 — Corrigir malformações de sintaxe .po") print(f"{'─'*65}") run_script(FIX_MALFORMED, po_files) # --- PASSO 2: Corrigir PT-BR → PT-PT --- print(f"\n{'─'*65}") print(f" PASSO 2/3 — Corrigir PT-BR → PT-PT") print(f"{'─'*65}") run_script(FIX_PTBR, po_files) # --- PASSO 3: Traduzir strings em falta --- if not args.skip_translate and not args.only_ptbr: engine = 'LibreTranslate' if args.no_deepl else 'DeepL PT-PT' print(f"\n{'─'*65}") print(f" PASSO 3/4 — Traduzir strings em falta via {engine}") print(f"{'─'*65}") extra = ['--dry-run'] if args.dry_run else [] run_script(TRANSLATE_MISSING, po_files, extra) # 2ª passagem fix_ptbr — DeepL pode introduzir padrões PT-BR no output if not args.dry_run: print(f"\n{'─'*65}") print(f" PASSO 3.5/4 — Fix PT-BR pós-tradução (2ª passagem)") print(f"{'─'*65}") run_script(FIX_PTBR, po_files) else: print(f"\n PASSO 3/4 — Tradução de strings: SALTADO") # --- PASSO 4: Recompilar todos os .mo --- if not args.dry_run: print(f"\n{'─'*65}") print(f" PASSO 4/4 — Recompilar .mo") print(f"{'─'*65}") mo_ok = 0 mo_err = 0 mo_errors = [] for po in po_files: ok = compile_mo(po) if ok: mo_ok += 1 else: mo_err += 1 mo_errors.append(po.name) print(f" .mo compilados: {mo_ok} OK | {mo_err} com erro") if mo_errors: print(f" Erros: {', '.join(mo_errors[:5])}") # --- RELATÓRIO FINAL --- elapsed = time.time() - start_time print(f"\n{'='*65}") print(f" RELATÓRIO FINAL") print(f"{'='*65}") total_strings = 0 total_empty = 0 plugins_100 = 0 plugins_partial = 0 for po in po_files: total, empty = count_empty_strings(po) total_strings += total total_empty += empty if empty == 0: plugins_100 += 1 else: plugins_partial += 1 coverage = ((total_strings - total_empty) / total_strings * 100) if total_strings > 0 else 0 print(f" Plugins processados: {len(po_files)}") print(f" 100% traduzidos: {plugins_100}") print(f" Com strings em falta: {plugins_partial}") print(f" Strings totais: {total_strings}") print(f" Strings em falta: {total_empty}") print(f" Cobertura: {coverage:.1f}%") print(f" Tempo total: {elapsed:.0f}s") print(f"{'='*65}\n") # Limpar backups temporários if not args.dry_run: bak_count = 0 for bak in library.rglob('*.bak_*'): bak.unlink() bak_count += 1 if bak_count: print(f" {bak_count} ficheiros de backup temporário removidos") if plugins_partial > 0: print(f"\n Plugins com strings ainda em falta:") for po in po_files: _, empty = count_empty_strings(po) if empty > 0: print(f" - {po.parent.name}: {empty} string(s) em falta") if __name__ == '__main__': main()