237 lines
8.0 KiB
Python
Executable File
237 lines
8.0 KiB
Python
Executable File
#!/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()
|