init: scripts diversos (crawlers, conversores, scrapers)

This commit is contained in:
2026-03-05 20:38:36 +00:00
commit 6ac6f4be2a
925 changed files with 850330 additions and 0 deletions

View File

@@ -0,0 +1,958 @@
#!/usr/bin/env python3
"""
WordPress Plugin .po Translation Script
Translates WordPress .po files to PT-PT using LibreTranslate with medical/business glossary.
Author: Descomplicar® Crescimento Digital
Date: 2026-02-11
Version: 1.0.0
Usage:
python3 translate-wordpress-plugin.py input.po [--batch-size 50] [--api-url https://translate.descomplicar.pt]
Features:
- Translates only empty msgstr entries
- Preserves placeholders (%s, %d, %1$s, etc.)
- Preserves HTML tags
- Applies PT-PT glossary post-translation
- Handles plural forms correctly
- Validates translations before saving
- Compiles .mo file automatically
- Progress tracking with ETA
"""
import re
import sys
import json
import argparse
import subprocess
from pathlib import Path
from typing import Dict, List, Tuple
from urllib.request import Request, urlopen
from urllib.error import URLError
import time
# PT-PT Glossary (EN→PT-PT) - Source: Guia-PT-PT.md v2.0
GLOSSARY = {
# --- Medical terminology ---
"appointment": "consulta",
"appointments": "consultas",
"encounter": "encontro clínico",
"encounters": "encontros clínicos",
"patient": "paciente",
"patients": "pacientes",
"doctor": "médico",
"doctors": "médicos",
"clinic": "clínica",
"clinics": "clínicas",
"prescription": "receita",
"prescriptions": "receitas",
"medical record": "registo clínico",
"medical records": "registos clínicos",
"health center": "centro de saúde",
"family doctor": "médico de família",
"general practitioner": "médico de família",
"emergency room": "consulta de urgência",
"ultrasound": "ecografia",
"CT scan": "TAC",
"blood tests": "análises",
"blood test": "análises",
"medical certificate": "atestado médico",
"vaccination record": "boletim de vacinas",
"clinical report": "relatório clínico",
"orthopedics": "ortopedia",
"ophthalmology": "oftalmologia",
"disorder": "perturbação",
"psychological assessment": "avaliação psicológica",
"therapy": "psicoterapia",
# --- Billing / Finance ---
"billing": "facturação",
"invoice": "factura",
"invoices": "facturas",
"bill": "factura",
"bills": "facturas",
"credit note": "nota de crédito",
"credit notes": "notas de crédito",
"receipt": "recibo",
"receipts": "recibos",
"purchase order": "nota de encomenda",
"delivery note": "guia de remessa",
"installment": "prestação",
"installments": "prestações",
"tax": "IVA",
"VAT": "IVA",
"bank transfer": "transferência bancária",
"balance sheet": "balanço",
"profit and loss": "demonstração de resultados",
"budget": "orçamento",
"quote": "orçamento",
"quotation": "orçamento",
"interest rate": "taxa de juro",
"stock exchange": "bolsa de valores",
"overdraft": "descoberto autorizado",
# --- UI Common ---
"dashboard": "painel",
"settings": "definições",
"preferences": "preferências",
"save": "guardar",
"delete": "eliminar",
"remove": "remover",
"cancel": "cancelar",
"search": "pesquisar",
"filter": "filtrar",
"sort": "ordenar",
"add new": "adicionar novo",
"add": "adicionar",
"edit": "editar",
"view": "ver",
"preview": "pré-visualizar",
"status": "estado",
"login": "iniciar sessão",
"log in": "iniciar sessão",
"sign in": "iniciar sessão",
"logout": "terminar sessão",
"log out": "terminar sessão",
"sign out": "terminar sessão",
"sign up": "registar-se",
"register": "registar",
"password": "palavra-passe",
"username": "nome de utilizador",
"display name": "nome de apresentação",
"permalink": "ligação permanente",
"upload": "carregar",
"download": "transferir",
"file": "ficheiro",
"files": "ficheiros",
"folder": "pasta",
"folders": "pastas",
"screen": "ecrã",
"share": "partilhar",
"shared": "partilhado",
"link": "hiperligação",
"hyperlink": "hiperligação",
"mouse": "rato",
"keyboard": "teclado",
"hard drive": "disco rígido",
"database": "base de dados",
"backup": "cópia de segurança",
"restore": "restaurar",
"update": "actualizar",
"updates": "actualizações",
"upgrade": "actualizar",
"submit": "submeter",
"confirm": "confirmar",
"approve": "aprovar",
"reject": "rejeitar",
"close": "fechar",
"open": "abrir",
"enable": "activar",
"disable": "desactivar",
"enabled": "activado",
"disabled": "desactivado",
"select": "seleccionar",
"selected": "seleccionado",
"collapse": "recolher",
"expand": "expandir",
"loading": "a carregar",
"processing": "a processar",
"pending": "pendente",
"completed": "concluído",
"failed": "falhado",
"error": "erro",
"warning": "aviso",
"success": "sucesso",
"notification": "notificação",
"notifications": "notificações",
"overview": "visão geral",
"details": "detalhes",
"summary": "resumo",
"history": "histórico",
"report": "relatório",
"reports": "relatórios",
"export": "exportar",
"import": "importar",
# --- E-commerce / WooCommerce ---
"cart": "carrinho",
"checkout": "finalizar compra",
"shipping": "envio",
"delivery": "entrega",
"coupon": "cupão",
"coupons": "cupões",
"discount": "desconto",
"discounts": "descontos",
"order": "encomenda",
"orders": "encomendas",
"product": "produto",
"products": "produtos",
"category": "categoria",
"categories": "categorias",
"tag": "etiqueta",
"tags": "etiquetas",
"stock": "stock",
"in stock": "em stock",
"out of stock": "sem stock",
"wishlist": "lista de desejos",
"refund": "reembolso",
"refunds": "reembolsos",
"return": "devolução",
"returns": "devoluções",
# --- Professional / Business ---
"employee": "colaborador",
"employees": "colaboradores",
"staff": "pessoal",
"salary": "ordenado",
"role": "função",
"meeting": "reunião",
"meetings": "reuniões",
"resume": "curriculum vitae",
"company": "empresa",
"team": "equipa",
"teams": "equipas",
"customer": "cliente",
"customers": "clientes",
"client": "cliente",
"clients": "clientes",
"supplier": "fornecedor",
"suppliers": "fornecedores",
"lead": "contacto",
"leads": "contactos",
"deal": "negócio",
"deals": "negócios",
"pipeline": "pipeline",
"task": "tarefa",
"tasks": "tarefas",
"project": "projecto",
"projects": "projectos",
"milestone": "marco",
"milestones": "marcos",
"deadline": "prazo",
"deadlines": "prazos",
"priority": "prioridade",
"assigned to": "atribuído a",
"workflow": "fluxo de trabalho",
# --- Technology / IT ---
"machine learning": "aprendizagem automática",
"big data": "dados massivos",
"neural network": "rede neuronal",
"neural networks": "redes neuronais",
"cloud computing": "computação em nuvem",
"cybersecurity": "cibersegurança",
"encryption": "encriptação",
"firewall": "firewall",
"authentication": "autenticação",
"container": "contentor",
"containers": "contentores",
"deployment": "implantação",
"version control": "controlo de versões",
"unit tests": "testes unitários",
"continuous integration": "integração contínua",
"continuous delivery": "entrega contínua",
"scalability": "escalabilidade",
"user experience": "experiência do utilizador",
"accessibility": "acessibilidade",
"responsive": "responsivo",
# --- Marketing / Digital ---
"target audience": "público-alvo",
"website": "sítio web",
"website traffic": "tráfego do sítio web",
"conversion rate": "taxa de conversão",
"market share": "quota de mercado",
"SEO": "SEO",
"campaign": "campanha",
"campaigns": "campanhas",
"newsletter": "boletim informativo",
"subscriber": "subscritor",
"subscribers": "subscritores",
"unsubscribe": "cancelar subscrição",
# --- Common phrases ---
"is required": "é obrigatório",
"must be": "deve ser",
"cannot be empty": "não pode estar vazio",
"at least": "pelo menos",
"cannot exceed": "não pode exceder",
"already exists": "já existe",
"not found": "não encontrado",
"retrieved successfully": "obtido com sucesso",
"saved successfully": "guardado com sucesso",
"updated successfully": "actualizado com sucesso",
"deleted successfully": "eliminado com sucesso",
"created successfully": "criado com sucesso",
"failed to": "falha ao",
"are you sure": "tem a certeza",
"this action cannot be undone": "esta acção não pode ser desfeita",
"no results found": "nenhum resultado encontrado",
"please try again": "por favor tente novamente",
"access denied": "acesso negado",
"permission denied": "permissão negada",
"you do not have permission": "não tem permissão",
"invalid": "inválido",
"optional": "opcional",
"learn more": "saber mais",
"read more": "ler mais",
"show more": "mostrar mais",
"show less": "mostrar menos",
"go back": "voltar",
"next": "seguinte",
"previous": "anterior",
"first": "primeiro",
"last": "último",
# --- Date/time ---
"today": "hoje",
"yesterday": "ontem",
"tomorrow": "amanhã",
"this week": "esta semana",
"last week": "semana passada",
"this month": "este mês",
"last month": "mês passado",
"this year": "este ano",
"daily": "diário",
"weekly": "semanal",
"monthly": "mensal",
"yearly": "anual",
"schedule": "agenda",
"scheduled": "agendado",
}
# PT-PT orthography fixes (BR→PT-PT) - Source: Guia-PT-PT.md v2.0
ORTHOGRAPHY_FIXES = {
# --- Orthographic differences (AO90 BR vs traditional PT-PT) ---
"atualizar": "actualizar",
"atualizad": "actualizad",
"atualização": "actualização",
"atualizações": "actualizações",
"fatura": "factura",
"faturas": "facturas",
"faturação": "facturação",
"faturamento": "facturação",
"selecionar": "seleccionar",
"selecionad": "seleccionad",
"seleção": "selecção",
"seleções": "selecções",
"ótimo": "óptimo",
"ação": "acção",
"ações": "acções",
"direção": "direcção",
"direções": "direcções",
"proteção": "protecção",
"coleção": "colecção",
"correção": "correcção",
"conexão": "conexão",
"detecção": "detecção",
"inspecção": "inspecção",
"recepção": "recepção",
"exceção": "excepção",
"infecção": "infecção",
"objeção": "objecção",
"projeção": "projecção",
"projecto": "projecto",
"contacto": "contacto",
"facto": "facto",
"exacto": "exacto",
"directo": "directo",
"correcto": "correcto",
# --- Vocabulary differences (BR→PT-PT) ---
# Tech / UI
"acessar": "aceder",
"arquivo": "ficheiro",
"arquivos": "ficheiros",
"diretório": "pasta",
"senha": "palavra-passe",
"senhas": "palavras-passe",
"tela": "ecrã",
"telas": "ecrãs",
"compartilhar": "partilhar",
"compartilhad": "partilhad",
"compartilhamento": "partilha",
"baixar": "transferir",
"fazer download": "transferir",
"configuração": "definição",
"configurações": "definições",
"configurar": "definir",
"colapsar": "recolher",
"banco de dados": "base de dados",
# Common verbs
"salvar": "guardar",
"deletar": "eliminar",
"deletad": "eliminad",
"cancelamento": "cancelamento",
"clicar": "clicar",
"apertar": "premir",
"digitar": "escrever",
"rodar": "executar",
"fechar sessão": "terminar sessão",
"fazer login": "iniciar sessão",
"fazer logout": "terminar sessão",
"cadastrar": "registar",
"cadastro": "registo",
# Everyday / general
"ônibus": "autocarro",
"trem": "comboio",
"banheiro": "casa de banho",
"café da manhã": "pequeno-almoço",
"geladeira": "frigorífico",
"celular": "telemóvel",
"pedágio": "portagem",
"açougue": "talho",
"estacionamento": "parque de estacionamento",
"carteira de motorista": "carta de condução",
"fila": "fila",
"nota fiscal": "factura",
"cupom fiscal": "talão",
"caixa eletrônico": "Multibanco",
"parcela": "prestação",
"parcelas": "prestações",
# Professional
"funcionário": "colaborador",
"funcionários": "colaboradores",
"salário": "ordenado",
"cargo": "função",
"currículo": "curriculum vitae",
"time": "equipa",
"equipe": "equipa",
# Education
"mensalidade": "propina",
"graduação": "licenciatura",
"doutorado": "doutoramento",
"matéria": "disciplina",
# Sports / Culture
"goleiro": "guarda-redes",
"chute": "pontapé",
"gol": "golo",
"pôster": "cartaz",
"temporada": "época",
# Food / Services
"cardápio": "ementa",
"garçom": "empregado de mesa",
"conta": "conta",
"aluguel": "aluguer",
# Legal / Admin
"réu": "arguido",
"cartório": "conservatória",
"prefeitura": "Câmara Municipal",
"subprefeitura": "Junta de Freguesia",
# Weather
"neblina": "nevoeiro",
"pancadas de chuva": "aguaceiros",
# Health
"exames de sangue": "análises",
"pronto-socorro": "consulta de urgência",
"posto de saúde": "centro de saúde",
"clínico geral": "médico de família",
"ultrassom": "ecografia",
"transtorno": "perturbação",
"transtornos": "perturbações",
# Environment / Science
"mudanças climáticas": "alterações climáticas",
"pegada de carbono": "pegada carbónica",
"rede neural": "rede neuronal",
"redes neurais": "redes neuronais",
"impressão 3D": "fabricação aditiva",
}
class LibreTranslateClient:
"""LibreTranslate API client."""
def __init__(self, api_url: str = "https://translate.descomplicar.pt"):
self.api_url = api_url.rstrip("/")
self.translate_endpoint = f"{self.api_url}/translate"
def translate(self, text: str, source: str = "en", target: str = "pt") -> str:
"""Translate text using LibreTranslate API."""
data = json.dumps({
"q": text,
"source": source,
"target": target,
"format": "text"
}).encode('utf-8')
req = Request(
self.translate_endpoint,
data=data,
headers={"Content-Type": "application/json"}
)
try:
with urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return result.get("translatedText", "")
except URLError as e:
print(f"❌ Translation API error: {e}")
return ""
except Exception as e:
print(f"❌ Unexpected error: {e}")
return ""
class PoFileTranslator:
"""WordPress .po file translator with validation."""
def __init__(self, po_file: Path, api_url: str, batch_size: int = 50):
self.po_file = po_file
self.batch_size = batch_size
self.translator = LibreTranslateClient(api_url)
self.stats = {
"total": 0,
"translated": 0,
"skipped": 0,
"failed": 0
}
def extract_entries(self) -> List[Dict]:
"""Extract all msgid/msgstr entries from .po file."""
entries = []
current_entry = {}
with open(self.po_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
for i, line in enumerate(lines):
line = line.rstrip('\n')
# Comment/reference line
if line.startswith('#'):
if 'reference' not in current_entry:
current_entry['reference'] = []
current_entry['reference'].append(line)
current_entry['line_start'] = i
# msgid
elif line.startswith('msgid '):
current_entry['msgid'] = self._extract_string(line)
current_entry['msgid_line'] = i
# msgid_plural
elif line.startswith('msgid_plural '):
current_entry['msgid_plural'] = self._extract_string(line)
# msgstr
elif line.startswith('msgstr ') or line.startswith('msgstr['):
msgstr_value = self._extract_string(line)
if 'msgstr' not in current_entry:
current_entry['msgstr'] = {}
# Handle plural forms
if line.startswith('msgstr['):
plural_index = int(line[7])
current_entry['msgstr'][plural_index] = msgstr_value
else:
current_entry['msgstr'][0] = msgstr_value
current_entry['msgstr_line'] = i
# Continuation line
elif line.startswith('"') and current_entry:
# Append to last field
continuation = self._extract_string(line)
if 'msgstr_line' in current_entry and i > current_entry['msgstr_line']:
last_index = max(current_entry['msgstr'].keys())
current_entry['msgstr'][last_index] += continuation
elif 'msgid_line' in current_entry and i > current_entry['msgid_line']:
current_entry['msgid'] += continuation
# Empty line - entry complete
elif line == '' and current_entry:
if 'msgid' in current_entry and current_entry['msgid']:
entries.append(current_entry)
current_entry = {}
# Add last entry if exists
if current_entry and 'msgid' in current_entry:
entries.append(current_entry)
return entries
def _extract_string(self, line: str) -> str:
"""Extract string from msgid/msgstr line."""
match = re.search(r'"(.*?)"', line)
return match.group(1) if match else ""
def needs_translation(self, entry: Dict) -> bool:
"""Check if entry needs translation."""
if not entry.get('msgid'):
return False
# Check if msgstr is empty
msgstr_dict = entry.get('msgstr', {})
if not msgstr_dict:
return True
# Check if all plural forms are empty
return all(not v for v in msgstr_dict.values())
def extract_placeholders(self, text: str) -> List[str]:
"""Extract placeholders like %s, %d, %1$s from text."""
return re.findall(r'%(?:\d+\$)?[sdifuxX]', text)
def extract_html_tags(self, text: str) -> List[str]:
"""Extract HTML tags from text."""
return re.findall(r'<[^>]+>', text)
def apply_glossary(self, text: str) -> str:
"""Apply PT-PT glossary to translated text."""
for en, pt in GLOSSARY.items():
# Case-insensitive replacement
text = re.sub(r'\b' + re.escape(en) + r'\b', pt, text, flags=re.IGNORECASE)
return text
def apply_orthography(self, text: str) -> str:
"""Fix Brazilian Portuguese to European Portuguese."""
for br, pt in ORTHOGRAPHY_FIXES.items():
text = re.sub(r'\b' + re.escape(br), pt, text, flags=re.IGNORECASE)
return text
def validate_translation(self, original: str, translated: str, strict_html: bool = True) -> Tuple[bool, str]:
"""Validate translated text preserves placeholders and HTML."""
# Check placeholders (always strict)
orig_placeholders = self.extract_placeholders(original)
trans_placeholders = self.extract_placeholders(translated)
if sorted(orig_placeholders) != sorted(trans_placeholders):
return False, f"Placeholder mismatch: {orig_placeholders} vs {trans_placeholders}"
# Check HTML tags (optional strict mode)
if strict_html:
orig_html = self.extract_html_tags(original)
trans_html = self.extract_html_tags(translated)
if sorted(orig_html) != sorted(trans_html):
# Warning only - try to auto-fix first
print(f" ⚠️ HTML tags differ but auto-fixing...")
return True, "OK"
def translate_text(self, text: str) -> str:
"""Translate text with glossary and validation."""
if not text or text.isspace():
return text
# Translate via LibreTranslate
translated = self.translator.translate(text)
if not translated:
return ""
# Apply glossary aggressively (multiple passes)
for _ in range(2): # Two passes to catch variations
translated = self.apply_glossary(translated)
translated = self.apply_orthography(translated)
# Fix common LibreTranslate mistakes
translated = self.fix_common_mistakes(translated)
# Fix translated HTML tags
translated = self.fix_html_tags(translated)
# Validate (only placeholders, HTML fixing is automatic)
valid, error = self.validate_translation(text, translated, strict_html=False)
if not valid:
print(f"⚠️ Validation failed: {error}")
print(f" Original: {text[:50]}...")
print(f" Translation: {translated[:50]}...")
return ""
return translated
def fix_html_tags(self, text: str) -> str:
"""Fix common HTML tag translations."""
html_fixes = {
# LibreTranslate often translates these
r'<forte>': '<strong>',
r'</forte>': '</strong>',
r'<em>': '<em>', # Keep
r'</em>': '</em>', # Keep
r'<quebra>': '<br>',
r'<parágrafo>': '<p>',
r'</parágrafo>': '</p>',
}
for wrong, correct in html_fixes.items():
text = re.sub(wrong, correct, text, flags=re.IGNORECASE)
return text
def fix_common_mistakes(self, text: str) -> str:
"""Fix common translation mistakes from LibreTranslate."""
fixes = {
# --- BR vocabulary that LibreTranslate produces ---
r'\bcompromissos\b': 'consultas', # appointments
r'\bsenha\b': 'palavra-passe', # password
r'\bsenhas\b': 'palavras-passe', # passwords
r'\bconfiguração\b': 'definição', # setting
r'\bconfigurações\b': 'definições', # settings
r'\bsalvar\b': 'guardar', # save
r'\bsalvo\b': 'guardado', # saved
r'\bsalva\b': 'guardada', # saved (f)
r'\bdeletar\b': 'eliminar', # delete
r'\bdeletado\b': 'eliminado', # deleted
r'\bdeletada\b': 'eliminada', # deleted (f)
r'\bacessar\b': 'aceder', # access
r'\bacessado\b': 'acedido', # accessed
r'\bcompartilhar\b': 'partilhar', # share
r'\bcompartilhado\b': 'partilhado', # shared
r'\bbaixar\b': 'transferir', # download
r'\bcadastrar\b': 'registar', # register
r'\bcadastro\b': 'registo', # registration
r'\bcadastrado\b': 'registado', # registered
r'\bcelular\b': 'telemóvel', # mobile phone
r'\bgerenciar\b': 'gerir', # manage
r'\bgerenciamento\b': 'gestão', # management
r'\bgerenciado\b': 'gerido', # managed
r'\bimplementar\b': 'implementar', # implement (keep)
r'\botimizar\b': 'optimizar', # optimize
# --- "required" variations ---
r'\bnecessária\b': 'obrigatória',
r'\bnecessário\b': 'obrigatório',
r'\brequerido\b': 'obrigatório',
r'\brequerida\b': 'obrigatória',
# --- Gerund → "estar a + infinitivo" (most common LibreTranslate error) ---
r'\bestá sendo\b': 'está a ser',
r'\bestão sendo\b': 'estão a ser',
r'\bestá fazendo\b': 'está a fazer',
r'\bestá criando\b': 'está a criar',
r'\bestá processando\b': 'está a processar',
r'\bestá carregando\b': 'está a carregar',
r'\bestá enviando\b': 'está a enviar',
r'\bestá atualizando\b': 'está a actualizar',
r'\bestá gerando\b': 'está a gerar',
r'\bestá excluindo\b': 'está a eliminar',
r'\bestá salvando\b': 'está a guardar',
r'\bestá deletando\b': 'está a eliminar',
r'\bestá baixando\b': 'está a transferir',
r'\bestá importando\b': 'está a importar',
r'\bestá exportando\b': 'está a exportar',
r'\bestá verificando\b': 'está a verificar',
r'\bestá calculando\b': 'está a calcular',
r'\bestá sincronizando\b': 'está a sincronizar',
r'\bestá conectando\b': 'está a ligar',
# --- Generic gerund catch-all (careful, only common patterns) ---
r'\bprocessando\b': 'a processar',
r'\bcarregando\b': 'a carregar',
r'\batualizando\b': 'a actualizar',
r'\bgerando\b': 'a gerar',
# --- Pronoun placement (proclisis→enclisis) ---
r'\bse registrar\b': 'registar-se',
r'\bse cadastrar\b': 'registar-se',
r'\bse conectar\b': 'ligar-se',
r'\bse inscrever\b': 'inscrever-se',
# --- Other common LibreTranslate mistakes ---
r'\bvocê\b': 'o utilizador', # "você" → neutral
r'\bnenhum resultado\b': 'nenhum resultado', # keep
r'\bpor favor\b': 'por favor', # keep
}
for wrong, correct in fixes.items():
text = re.sub(wrong, correct, text, flags=re.IGNORECASE)
return text
def translate_entries(self) -> List[Dict]:
"""Translate all entries needing translation."""
entries = self.extract_entries()
self.stats['total'] = len(entries)
print(f"\n📊 Found {self.stats['total']} entries in {self.po_file.name}")
to_translate = [e for e in entries if self.needs_translation(e)]
print(f"🔄 {len(to_translate)} entries need translation\n")
if not to_translate:
print("✅ All entries already translated!")
return entries
# Translate in batches
start_time = time.time()
for i, entry in enumerate(to_translate, 1):
msgid = entry['msgid']
# Progress
elapsed = time.time() - start_time
rate = i / elapsed if elapsed > 0 else 0
eta = (len(to_translate) - i) / rate if rate > 0 else 0
print(f"[{i}/{len(to_translate)}] Translating... (ETA: {eta:.0f}s)")
print(f" EN: {msgid[:60]}...")
# Translate msgid
translated = self.translate_text(msgid)
if translated:
entry['msgstr'][0] = translated
print(f" PT: {translated[:60]}...")
self.stats['translated'] += 1
else:
print(f" ❌ Translation failed")
self.stats['failed'] += 1
# Translate plural form if exists
if 'msgid_plural' in entry:
msgid_plural = entry['msgid_plural']
translated_plural = self.translate_text(msgid_plural)
if translated_plural:
entry['msgstr'][1] = translated_plural
# Rate limiting
time.sleep(0.1)
print(f"\n✅ Translation complete in {time.time() - start_time:.1f}s")
return entries
def write_po_file(self, entries: List[Dict], output_file: Path):
"""Write translated entries back to .po file."""
with open(self.po_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Update msgstr lines
for entry in entries:
if 'msgstr_line' in entry and entry.get('msgstr'):
msgstr_dict = entry['msgstr']
# Single msgstr
if len(msgstr_dict) == 1 and 0 in msgstr_dict:
msgstr_line = entry['msgstr_line']
lines[msgstr_line] = f'msgstr "{msgstr_dict[0]}"\n'
# Plural msgstr
else:
base_line = entry['msgstr_line']
for idx, value in msgstr_dict.items():
# Find or create msgstr[N] line
line_offset = idx
if base_line + line_offset < len(lines):
lines[base_line + line_offset] = f'msgstr[{idx}] "{value}"\n'
# Write to output
with open(output_file, 'w', encoding='utf-8') as f:
f.writelines(lines)
print(f"\n💾 Saved to: {output_file}")
def compile_mo(self, po_file: Path):
"""Compile .po to .mo using msgfmt."""
mo_file = po_file.with_suffix('.mo')
try:
result = subprocess.run(
['msgfmt', '-cv', '-o', str(mo_file), str(po_file)],
capture_output=True,
text=True,
check=True
)
print(f"\n✅ Compiled .mo file: {mo_file}")
print(result.stdout)
except subprocess.CalledProcessError as e:
print(f"\n❌ Compilation failed: {e.stderr}")
except FileNotFoundError:
print("\n⚠️ msgfmt not found. Install gettext: sudo apt install gettext")
def print_stats(self):
"""Print translation statistics."""
print("\n" + "="*60)
print("📊 TRANSLATION STATISTICS")
print("="*60)
print(f"Total entries: {self.stats['total']}")
print(f"Translated: {self.stats['translated']}")
print(f"Failed: {self.stats['failed']}")
print(f"Skipped: {self.stats['total'] - self.stats['translated'] - self.stats['failed']}")
print(f"Success rate: {self.stats['translated'] / self.stats['total'] * 100:.1f}%")
print("="*60)
def main():
parser = argparse.ArgumentParser(
description="Translate WordPress .po files to PT-PT using LibreTranslate"
)
parser.add_argument(
"po_file",
type=Path,
help="Path to .po file to translate"
)
parser.add_argument(
"--batch-size",
type=int,
default=50,
help="Number of entries to translate per batch (default: 50)"
)
parser.add_argument(
"--api-url",
type=str,
default="https://translate.descomplicar.pt",
help="LibreTranslate API URL"
)
parser.add_argument(
"--output",
type=Path,
help="Output .po file (default: overwrite input)"
)
args = parser.parse_args()
# Validate input file
if not args.po_file.exists():
print(f"❌ File not found: {args.po_file}")
sys.exit(1)
if not args.po_file.suffix == '.po':
print(f"❌ Not a .po file: {args.po_file}")
sys.exit(1)
# Output file
output_file = args.output or args.po_file
print("="*60)
print("🌍 WordPress Plugin Translation Script")
print("="*60)
print(f"Input: {args.po_file}")
print(f"Output: {output_file}")
print(f"API: {args.api_url}")
print("="*60)
# Translate
translator = PoFileTranslator(
po_file=args.po_file,
api_url=args.api_url,
batch_size=args.batch_size
)
entries = translator.translate_entries()
translator.write_po_file(entries, output_file)
translator.compile_mo(output_file)
translator.print_stats()
if __name__ == "__main__":
main()