Files
scripts/translate-wp/translate-po-v2.py

1052 lines
32 KiB
Python

#!/usr/bin/env python3
"""
Traduz e corrige ficheiros .po WordPress para PT-PT.
Versão 2.0 - Com protecção de marcas e glossário completo.
Funcionalidades:
- Protege nomes de plugins/marcas antes de enviar à API
- Traduz strings vazias via LibreTranslate (target: pt)
- Corrige termos PT-BR para PT-PT com glossário alargado
- Suporte a plural forms
- Python 3.6 compatível
Uso:
python3 translate-po-v2.py [opções] <ficheiro.po> [...]
Opções:
--dry-run Mostra o que faria sem gravar
--fix-only Só aplica correcções PT-BR→PT-PT (sem API)
--retranslate Re-traduz strings existentes suspeitas
"""
import sys
import os
import re
import json
import time
import urllib.request
import urllib.error
import subprocess
import shutil
TRANSLATE_API = "https://translate.descomplicar.pt/translate"
RATE_LIMIT_DELAY = 0.3 # segundos entre chamadas API
# =============================================================================
# TERMOS PROTEGIDOS (nunca traduzir)
# Ordem importa: mais longos primeiro para match correcto
# =============================================================================
PROTECTED_TERMS = [
# Plugins WordPress (marcas)
"Insert Headers and Footers",
"Advanced Custom Fields",
"Rank Math SEO Pro",
"Rank Math SEO",
"Rank Math",
"Google Analytics",
"Google Tag Manager",
"WooCommerce Subscriptions",
"WooCommerce Payments",
"WooCommerce",
"WPForms Lite",
"WPForms",
"Wordfence Security",
"Wordfence",
"UpdraftPlus Premium",
"UpdraftPlus",
"Fluent Forms",
"Fluent CRM",
"FluentCRM",
"Fluent SMTP",
"FluentSMTP",
"Elementor Pro",
"Elementor",
"ElementsKit Lite",
"ElementsKit",
"Happy Elementor Addons Pro",
"Happy Elementor Addons",
"Branda White Label",
"Branda",
"WPML",
"Polylang Pro",
"Polylang",
"Yoast SEO",
"Yoast",
"All in One SEO",
"MonsterInsights",
"WP Fastest Cache",
"Loco Translate",
"Contact Form 7",
"Mailchimp",
"HubSpot",
"JetEngine",
"JetElements",
"JetSmartFilters",
"Crocoblock",
"KiviCare",
"WP Rocket",
"Smush",
"WPMU DEV",
"Hummingbird",
"SeedProd",
"OptinMonster",
"TrustPulse",
"PushEngage",
"AffiliateWP",
"Easy Digital Downloads",
"MemberPress",
"LearnDash",
"LifterLMS",
"Ninja Forms",
"Gravity Forms",
"Formidable Forms",
"User Registration",
"ProfilePress",
"BuddyBoss",
"BuddyPress",
# Plataformas e serviços
"WordPress",
"WordPress.org",
"WordPress.com",
"WP-CLI",
"Gutenberg",
"ClassicPress",
"MySQL",
"MariaDB",
"PhpMyAdmin",
"phpMyAdmin",
"cPanel",
"Cloudflare",
"Stripe",
"PayPal",
"Multibanco",
"MB Way",
"MBWay",
"IFTHENPay",
"EuPago",
"Moloni",
"Facebook",
"Instagram",
"Twitter",
"LinkedIn",
"YouTube",
"WhatsApp",
"TikTok",
"Pinterest",
"Snapchat",
"Telegram",
"Discord",
"Slack",
"Zoom",
"Teams",
"Google",
"Gmail",
"Chrome",
"Firefox",
"Safari",
"GitHub",
"Gitea",
"Supabase",
"Amazon",
"AWS",
"Azure",
"Docker",
"Kubernetes",
# Termos técnicos web que ficam em inglês
"email",
"e-mail",
"URL",
"URI",
"SEO",
"API",
"REST API",
"REST",
"JSON",
"XML",
"HTML",
"CSS",
"JavaScript",
"jQuery",
"PHP",
"PHP-FPM",
"SSL",
"TLS",
"HTTPS",
"HTTP",
"FTP",
"SFTP",
"SSH",
"CDN",
"DNS",
"IP",
"AJAX",
"CRM",
"ERP",
"SaaS",
"plugin",
"plugins",
"widget",
"widgets",
"shortcode",
"shortcodes",
"hook",
"hooks",
"filter",
"filters",
"action",
"sidebar",
"header",
"footer",
"cache",
"spam",
"token",
"tokens",
"modal",
"tooltip",
"favicon",
"webhook",
"webhooks",
"checkout",
"admin",
"backend",
"frontend",
"debug",
"log",
"logs",
"cron",
"nonce",
"nonces",
"slug",
"slugs",
"feed",
"RSS",
"OPML",
"PDF",
"CSV",
"XML",
"JSON",
"PNG",
"JPG",
"JPEG",
"GIF",
"SVG",
"WebP",
"AVIF",
# Termos WordPress específicos (manter em inglês no contexto WP)
"Post",
"Posts",
"Page",
"Pages",
"Media",
"Dashboard",
"Multisite",
"Nonce",
"Permalink",
"Permalinks",
"Transient",
"Transients",
"WP_Query",
"get_posts",
"the_content",
]
# Compilar tokens para protecção
_TOKEN_PATTERN = "__PROT_{idx:04d}__"
def protect_terms(text):
"""Substitui termos protegidos por tokens. Retorna (texto_com_tokens, mapa)."""
token_map = {}
modified = text
idx = 0
for term in PROTECTED_TERMS:
# Procura case-sensitive primeiro, depois case-insensitive para siglas
pattern = re.escape(term)
matches = list(re.finditer(pattern, modified, re.IGNORECASE))
for match in reversed(matches): # reverse para não deslocar indices
token = _TOKEN_PATTERN.format(idx=idx)
token_map[token] = match.group(0) # preservar capitalização original
modified = modified[:match.start()] + token + modified[match.end():]
idx += 1
return modified, token_map
def restore_terms(text, token_map):
"""Restaura tokens com os termos originais."""
for token, original in token_map.items():
text = text.replace(token, original)
return text
# =============================================================================
# GLOSSÁRIO PT-BR → PT-PT
# =============================================================================
# Cada entrada: (padrão regex, substituição)
# Aplicado DEPOIS da tradução da API
PTBR_TO_PTPT = [
# --- EMAIL (crítico: múltiplas variantes) ---
# PT-PT usa "email" — correio eletrónico/eletrônico/electrónico/electronico
(r'[Cc]orre[io]+ [Ee]le[ct]tr?[oôó]nico', 'email'),
(r'[Cc]orre[io]+ [Ee]lectr[oôó]nico', 'email'),
(r'correio eletrónico', 'email'),
(r'Correio [Ee]letrónico', 'email'),
(r'correio electrónico', 'email'),
(r'Correio [Ee]lectrónico', 'email'),
(r'\bcaixa de correio\b', 'caixa de email'),
(r'\bCaixa de [Cc]orreio\b', 'Caixa de email'),
(r'endere[cç]o de e-mail', 'endereço de email'),
(r'Endere[cç]o de e-mail', 'Endereço de email'),
(r'endere[cç]o de correio electr[oôó]nico', 'endereço de email'),
(r'Endere[cç]o de correio electr[oôó]nico', 'Endereço de email'),
(r'endere[cç]o de correio eletr[oôó]nico', 'endereço de email'),
(r'Endere[cç]o de correio eletr[oôó]nico', 'Endereço de email'),
(r'\bE-mail\b', 'Email'),
(r'\be-mail\b', 'email'),
# --- PASSWORDS / AUTENTICAÇÃO ---
(r'\bSenha de acesso\b', 'Palavra-passe de acesso'),
(r'\bsenha de acesso\b', 'palavra-passe de acesso'),
(r'\bSenhas\b', 'Palavras-passe'),
(r'\bsenhas\b', 'palavras-passe'),
(r'\bSenha\b', 'Palavra-passe'),
(r'\bsenha\b', 'palavra-passe'),
# --- UTILIZADORES ---
(r'\bNome de [Uu]suári[oa]\b', 'Nome de utilizador'),
(r'\bnome de [Uu]suári[oa]\b', 'nome de utilizador'),
(r'\bUsuários\b', 'Utilizadores'),
(r'\busuários\b', 'utilizadores'),
(r'\bUsuário\b', 'Utilizador'),
(r'\busuário\b', 'utilizador'),
(r'\bUsuárias\b', 'Utilizadoras'),
(r'\busuárias\b', 'utilizadoras'),
(r'\bUsuária\b', 'Utilizadora'),
(r'\busuária\b', 'utilizadora'),
# --- FICHEIROS / PASTAS ---
(r'\bDiretórios\b', 'Pastas'),
(r'\bdiretórios\b', 'pastas'),
(r'\bDiretório\b', 'Pasta'),
(r'\bdiretório\b', 'pasta'),
(r'\bDiretórias\b', 'Pastas'),
(r'\bdiretórias\b', 'pastas'),
(r'\bDiretória\b', 'Pasta'),
(r'\bdiretória\b', 'pasta'),
(r'\bArquivos\b', 'Ficheiros'),
(r'\barquivos\b', 'ficheiros'),
(r'\bArquivo\b', 'Ficheiro'),
(r'\barquivo\b', 'ficheiro'),
# --- LIGAÇÕES / CONEXÕES ---
(r'\bConexões\b', 'Ligações'),
(r'\bconexões\b', 'ligações'),
(r'\bConexão\b', 'Ligação'),
(r'\bconexão\b', 'ligação'),
(r'\bDesconectado\b', 'Desligado'),
(r'\bdesconectado\b', 'desligado'),
(r'\bDesconectar\b', 'Desligar'),
(r'\bdesconectar\b', 'desligar'),
(r'\bDesconectados\b', 'Desligados'),
(r'\bdesconectados\b', 'desligados'),
(r'\bConectado\b', 'Ligado'),
(r'\bconectado\b', 'ligado'),
(r'\bConectar\b', 'Ligar'),
(r'\bconectar\b', 'ligar'),
(r'\bConectados\b', 'Ligados'),
(r'\bconectados\b', 'ligados'),
# --- GUARDAR / AÇÕES ---
(r'\bSalvar como\b', 'Guardar como'),
(r'\bsalvar como\b', 'guardar como'),
(r'\bSalvos\b', 'Guardados'),
(r'\bsalvos\b', 'guardados'),
(r'\bSalvo\b', 'Guardado'),
(r'\bsalvo\b', 'guardado'),
(r'\bSalvar\b', 'Guardar'),
(r'\bsalvar\b', 'guardar'),
(r'\bSalve\b', 'Guarde'),
(r'\bsalve\b', 'guarde'),
(r'\bBaixar\b', 'Transferir'),
(r'\bbaixar\b', 'transferir'),
(r'\bBaixado\b', 'Transferido'),
(r'\bbaixado\b', 'transferido'),
(r'\bCompartilhados\b', 'Partilhados'),
(r'\bcompartilhados\b', 'partilhados'),
(r'\bCompartilhado\b', 'Partilhado'),
(r'\bcompartilhado\b', 'partilhado'),
(r'\bCompartilhar\b', 'Partilhar'),
(r'\bcompartilhar\b', 'partilhar'),
# --- ECRÃ ---
(r'\bTelas\b', 'Ecrãs'),
(r'\btelas\b', 'ecrãs'),
(r'\bTela\b', 'Ecrã'),
(r'\btela\b', 'ecrã'),
# --- ACTUALIZAÇÕES ---
(r'\bAtualizações\b', 'Actualizações'),
(r'\batualizações\b', 'actualizações'),
(r'\bAtualização\b', 'Actualização'),
(r'\batualização\b', 'actualização'),
(r'\bAtualizados\b', 'Actualizados'),
(r'\batualizados\b', 'actualizados'),
(r'\bAtualizadas\b', 'Actualizadas'),
(r'\batualizadas\b', 'actualizadas'),
(r'\bAtualizado\b', 'Actualizado'),
(r'\batualizado\b', 'actualizado'),
(r'\bAtualizada\b', 'Actualizada'),
(r'\batualizada\b', 'actualizada'),
(r'\bAtualizar\b', 'Actualizar'),
(r'\batualizar\b', 'actualizar'),
(r'\bAtualize\b', 'Actualize'),
(r'\batualize\b', 'actualize'),
(r'\bAtualizando\b', 'A actualizar'),
(r'\batualizando\b', 'a actualizar'),
(r'\bDesatualizadas\b', 'Desactualizadas'),
(r'\bdesatualizadas\b', 'desactualizadas'),
(r'\bDesatualizada\b', 'Desactualizada'),
(r'\bdesatualizada\b', 'desactualizada'),
(r'\bDesatualizados\b', 'Desactualizados'),
(r'\bdesatualizados\b', 'desactualizados'),
(r'\bDesatualizado\b', 'Desactualizado'),
(r'\bdesatualizado\b', 'desactualizado'),
(r'\bDesatualizar\b', 'Desactualizar'),
(r'\bdesatualizar\b', 'desactualizar'),
# --- CONFIGURAÇÕES / DEFINIÇÕES (contexto UI) ---
# Atenção: "configurações" pode ser correcto em PT-PT em alguns contextos
# Substituir apenas quando contexto claramente é "Settings" de UI
(r'\bConfigurações\b', 'Definições'),
(r'\bconfigurações\b', 'definições'),
(r'\bConfiguração\b', 'Definição'),
(r'\bconfiguração\b', 'definição'),
# --- VOCÊ / TRATAMENTO ---
(r'\bVocês\b', 'Os utilizadores'),
(r'\bvocês\b', 'os utilizadores'),
(r'\bVocê\b', 'O utilizador'),
(r'\bvocê\b', 'o utilizador'),
# --- BACKUPS ---
(r'\bBackups\b', 'Cópias de segurança'),
(r'\bbackups\b', 'cópias de segurança'),
(r'\bBackup\b', 'Cópia de segurança'),
(r'\bbackup\b', 'cópia de segurança'),
# --- PAINEL ---
(r'\bPainel de controle\b', 'Painel de controlo'),
(r'\bpainel de controle\b', 'painel de controlo'),
# --- CONTACTO (PT-BR usa "contato" sem c) ---
(r'\bContatos\b', 'Contactos'),
(r'\bcontatos\b', 'contactos'),
(r'\bContato\b', 'Contacto'),
(r'\bcontato\b', 'contacto'),
(r'\bSubusuários\b', 'Sub-utilizadores'),
(r'\bsubusuários\b', 'sub-utilizadores'),
(r'\bSubusuário\b', 'Sub-utilizador'),
(r'\bsubusuário\b', 'sub-utilizador'),
# --- GERENCIAR (PT-BR) → GERIR (PT-PT) ---
(r'\bGerenciam\b', 'Gerem'),
(r'\bgerenciam\b', 'gerem'),
(r'\bGerenciar\b', 'Gerir'),
(r'\bgerenciar\b', 'gerir'),
(r'\bGerenciem\b', 'Giram'),
(r'\bgerenciem\b', 'giram'),
(r'\bGerencia\b', 'Gere'),
(r'\bgerencia\b', 'gere'),
(r'\bGerenciamento\b', 'Gestão'),
(r'\bgerenciamento\b', 'gestão'),
(r'\bGerenciados\b', 'Geridos'),
(r'\bgerenciados\b', 'geridos'),
(r'\bGerenciado\b', 'Gerido'),
(r'\bgerenciado\b', 'gerido'),
# --- REACTIVAR (PT-BR: reativar) ---
(r'\bReativação\b', 'Reactivação'),
(r'\breativação\b', 'reactivação'),
(r'\bReativado\b', 'Reactivado'),
(r'\breativado\b', 'reactivado'),
(r'\bReativar\b', 'Reactivar'),
(r'\breativar\b', 'reactivar'),
(r'\bReative\b', 'Reactive'),
(r'\breative\b', 'reactive'),
# --- VARREDURA (PT-BR) → VERIFICAÇÃO (PT-PT) ---
(r'\bVarreduras\b', 'Verificações'),
(r'\bvarreduras\b', 'verificações'),
(r'\bVarredura\b', 'Verificação'),
(r'\bvarredura\b', 'verificação'),
(r'\bVarrer\b', 'Verificar'),
(r'\bvarrer\b', 'verificar'),
# --- EXPRESSÕES COMUNS ---
(r'\bTem certeza\b', 'Tem a certeza'),
(r'\btem certeza\b', 'tem a certeza'),
(r'\bTem a certeza\b', 'Tem a certeza'), # já correcto
(r'\bcontêiner\b', 'contentor'),
(r'\bContêiner\b', 'Contentor'),
(r'\bcontêineres\b', 'contentores'),
(r'\bContêineres\b', 'Contentores'),
(r'\bAtualizado para\b', 'Actualizado para'),
(r'\batualizado para\b', 'actualizado para'),
(r'\batualizar para\b', 'actualizar para'),
(r'\bAtualizar para\b', 'Actualizar para'),
(r'\batualize para\b', 'actualize para'),
(r'\bAtualize para\b', 'Actualize para'),
# --- ACTIVO (PT-BR: ativo sem c) ---
(r'\bAtivos\b', 'Activos'),
(r'\bativos\b', 'activos'),
(r'\bAtivas\b', 'Activas'),
(r'\bativas\b', 'activas'),
(r'\bAtivo\b', 'Activo'),
(r'\bativo\b', 'activo'),
(r'\bAtiva\b', 'Activa'),
(r'\bativa\b', 'activa'),
(r'\bInativo\b', 'Inactivo'),
(r'\binativo\b', 'inactivo'),
(r'\bInativos\b', 'Inactivos'),
(r'\binativos\b', 'inactivos'),
(r'\bInativas\b', 'Inactivas'),
(r'\binativas\b', 'inactivas'),
# --- REGISTOS (PT-BR: registros) ---
(r'\bRegistros\b', 'Registos'),
(r'\bregistros\b', 'registos'),
(r'\bRegistro\b', 'Registo'),
(r'\bregistro\b', 'registo'),
# --- ABA / SEPARADOR (browser tab) ---
(r'\babre em uma nova aba\b', 'abre num novo separador'),
(r'\bAbre em uma nova aba\b', 'Abre num novo separador'),
(r'\babre em uma nova guia\b', 'abre num novo separador'),
(r'\babre em uma nova janela\b', 'abre numa nova janela'),
(r'\babre em nova aba\b', 'abre num novo separador'),
(r'\bnova aba\b', 'novo separador'),
(r'\bNova aba\b', 'Novo separador'),
(r'\bnovas abas\b', 'novos separadores'),
(r'\bnova guia\b', 'novo separador'),
(r'\bNova guia\b', 'Novo separador'),
# --- EXCLUIRÁ / FUTURE TENSE (eliminar) ---
(r'\bExcluirão\b', 'Eliminarão'),
(r'\bexcluirão\b', 'eliminarão'),
(r'\bExcluirá\b', 'Eliminará'),
(r'\bexcluirá\b', 'eliminará'),
# --- NÃO SALVAS/SALVO ---
(r'\bnão salvas\b', 'não guardadas'),
(r'\bNão salvas\b', 'Não guardadas'),
(r'\bnão salvo\b', 'não guardado'),
(r'\bNão salvo\b', 'Não guardado'),
(r'\bnão salvos\b', 'não guardados'),
# --- PREPOSIÇÃO + POSSESSIVO (PT-BR sem artigo) ---
(r'\bem seu site\b', 'no seu site'),
(r'\bEm seu site\b', 'No seu site'),
(r'\bem seu\b', 'no seu'),
(r'\bEm seu\b', 'No seu'),
(r'\bde seu\b', 'do seu'),
(r'\bDe seu\b', 'Do seu'),
(r'\bpara seu\b', 'para o seu'),
(r'\bPara seu\b', 'Para o seu'),
(r'\bno seu\b', 'no seu'), # já correcto
# --- TELEMÓVEL (PT-BR: celular) ---
(r'\bCelulares\b', 'Telemóveis'),
(r'\bcelulares\b', 'telemóveis'),
(r'\bCelular\b', 'Telemóvel'),
(r'\bcelular\b', 'telemóvel'),
# --- CLIQUE EM (estilo PT-BR) ---
(r'\bClique em\b', 'Clique em'), # igual
(r'\bClique aqui\b', 'Clique aqui'), # igual
# --- GERÚNDIO → INFINITIVO PROGRESSIVO (PT-PT usa "estar a + inf") ---
(r'\bCarregando\b', 'A carregar'),
(r'\bcarregando\b', 'a carregar'),
(r'\bProcessando\b', 'A processar'),
(r'\bprocessando\b', 'a processar'),
(r'\bEnviando\b', 'A enviar'),
(r'\benviando\b', 'a enviar'),
(r'\bVerificando\b', 'A verificar'),
(r'\bverificando\b', 'a verificar'),
(r'\bSalvando\b', 'A guardar'),
(r'\bsalvando\b', 'a guardar'),
(r'\bImportando\b', 'A importar'),
(r'\bimportando\b', 'a importar'),
(r'\bExportando\b', 'A exportar'),
(r'\bexportando\b', 'a exportar'),
(r'\bInstalando\b', 'A instalar'),
(r'\binstalando\b', 'a instalar'),
(r'\bActivando\b', 'A activar'),
(r'\bactivando\b', 'a activar'),
(r'\bDesactivando\b', 'A desactivar'),
(r'\bdesactivando\b', 'a desactivar'),
(r'\bGerando\b', 'A gerar'),
(r'\bgerando\b', 'a gerar'),
(r'\bBuscando\b', 'A procurar'),
(r'\bbuscando\b', 'a procurar'),
(r'\bCriando\b', 'A criar'),
(r'\bcriando\b', 'a criar'),
(r'\bDeletando\b', 'A eliminar'),
(r'\bdeletando\b', 'a eliminar'),
(r'\bEliminando\b', 'A eliminar'),
(r'\beliminando\b', 'a eliminar'),
(r'\bApagando\b', 'A apagar'),
(r'\bapagando\b', 'a apagar'),
(r'\bSincronizando\b', 'A sincronizar'),
(r'\bsincronizando\b', 'a sincronizar'),
(r'\bConectando\b', 'A ligar'),
(r'\bconectando\b', 'a ligar'),
(r'\bDescarregando\b', 'A transferir'),
(r'\bdescarregando\b', 'a transferir'),
# --- BANCO DE DADOS / BASE DE DADOS ---
(r'\bbanco de dados\b', 'base de dados'),
(r'\bBanco de dados\b', 'Base de dados'),
(r'\bBanco de Dados\b', 'Base de Dados'),
(r'\bbancos de dados\b', 'bases de dados'),
(r'\bBancos de dados\b', 'Bases de dados'),
# Nota: possessivos "seu/sua" omitidos para evitar artigo duplo quando já existe "o seu"
# --- INSTALAÇÃO / ACTIVAÇÃO ---
(r'\bInstalação\b', 'Instalação'), # igual
(r'\bAtivação\b', 'Activação'),
(r'\bativação\b', 'activação'),
(r'\bAtivações\b', 'Activações'),
(r'\bativações\b', 'activações'),
(r'\bAtivado\b', 'Activado'),
(r'\bativado\b', 'activado'),
(r'\bAtivada\b', 'Activada'),
(r'\bativada\b', 'activada'),
(r'\bAtivados\b', 'Activados'),
(r'\bativados\b', 'activados'),
(r'\bAtivar\b', 'Activar'),
(r'\bativar\b', 'activar'),
(r'\bAtive\b', 'Active'),
(r'\bative\b', 'active'),
(r'\bDesativado\b', 'Desactivado'),
(r'\bdesativado\b', 'desactivado'),
(r'\bDesativada\b', 'Desactivada'),
(r'\bdesativada\b', 'desactivada'),
(r'\bDesativar\b', 'Desactivar'),
(r'\bdesativar\b', 'desactivar'),
(r'\bDesative\b', 'Desactive'),
(r'\bdesative\b', 'desactive'),
# --- MAIS TERMOS COMUNS EM PLUGINS WP ---
(r'\bClique\b', 'Clique'), # igual
(r'\bPor favor\b', 'Por favor'), # igual
(r'\bInformações\b', 'Informações'), # igual PT-PT
(r'\bNotificações\b', 'Notificações'), # igual
(r'\bIntegração\b', 'Integração'), # igual
(r'\bIntegrações\b', 'Integrações'), # igual
(r'\bMensagens\b', 'Mensagens'), # igual
(r'\bEsquecer\b', 'Esquecer'), # igual
(r'\bExcluído\b', 'Eliminado'),
(r'\bexcluído\b', 'eliminado'),
(r'\bExcluída\b', 'Eliminada'),
(r'\bexcluída\b', 'eliminada'),
(r'\bExcluídos\b', 'Eliminados'),
(r'\bexcluídos\b', 'eliminados'),
(r'\bExcluídas\b', 'Eliminadas'),
(r'\bexcluídas\b', 'eliminadas'),
(r'\bExcluir\b', 'Eliminar'),
(r'\bexcluir\b', 'eliminar'),
(r'\bExclua\b', 'Elimine'),
(r'\bexclua\b', 'elimine'),
(r'\bInserido\b', 'Inserido'), # igual
(r'\bInserir\b', 'Inserir'), # igual
(r'\bConcluído\b', 'Concluído'), # igual
(r'\bConcluída\b', 'Concluída'), # igual
(r'\bErro\b', 'Erro'), # igual
(r'\bFalhou\b', 'Falhou'), # igual
(r'\bSucesso\b', 'Sucesso'), # igual
# --- OUTROS VERBOS PT-BR ---
(r'\bDeletar\b', 'Eliminar'),
(r'\bdeletar\b', 'eliminar'),
(r'\bDeletados\b', 'Eliminados'),
(r'\bdeletados\b', 'eliminados'),
(r'\bDeletado\b', 'Eliminado'),
(r'\bdeletado\b', 'eliminado'),
(r'\bDeletar\b', 'Eliminar'),
(r'\bCancelar\b', 'Cancelar'), # igual
(r'\bBuscar\b', 'Procurar'),
(r'\bbuscar\b', 'procurar'),
(r'\bBusca\b', 'Procura'),
(r'\bbusca\b', 'procura'),
(r'\bClicar\b', 'Clicar'), # igual
(r'\bAcessar\b', 'Aceder'),
(r'\bacessar\b', 'aceder'),
(r'\bAcesse\b', 'Aceda'),
(r'\bacesse\b', 'aceda'),
(r'\bCadastrar\b', 'Registar'),
(r'\bcadastrar\b', 'registar'),
(r'\bCadastro\b', 'Registo'),
(r'\bcadastro\b', 'registo'),
(r'\bInstalar\b', 'Instalar'), # igual
# --- TERMOS DE UI ---
(r'\bBotão\b', 'Botão'), # igual
(r'\bCheckbox\b', 'Caixa de selecção'),
(r'\bcheckbox\b', 'caixa de selecção'),
(r'\bCheckboxes\b', 'Caixas de selecção'),
(r'\bcheckboxes\b', 'caixas de selecção'),
(r'\bBarra de progresso\b', 'Barra de progresso'), # igual
(r'\bPop-up\b', 'Pop-up'), # manter
(r'\bpop-up\b', 'pop-up'),
(r'\bPopup\b', 'Pop-up'),
(r'\bpopup\b', 'pop-up'),
(r'\bLayout\b', 'Layout'), # manter
(r'\blayout\b', 'layout'),
# --- TERMOS DE COMÉRCIO ---
(r'\bCupom\b', 'Cupão'),
(r'\bcupom\b', 'cupão'),
(r'\bCupons\b', 'Cupões'),
(r'\bcupons\b', 'cupões'),
(r'\bEstoque\b', 'Stock'),
(r'\bestoque\b', 'stock'),
(r'\bFrete\b', 'Envio'),
(r'\bfrete\b', 'envio'),
(r'\bCEP\b', 'Código Postal'),
(r'\bCpf\b', 'NIF'),
(r'\bCNPJ\b', 'NIPC'),
(r'\bNota [Ff]iscal\b', 'Factura'),
(r'\bnota fiscal\b', 'factura'),
(r'\bBoleto\b', 'Referência Multibanco'),
(r'\bboleto\b', 'referência Multibanco'),
(r'\bPix\b', 'MB Way'),
# --- ORTOGRAFIA PT-PT vs PT-BR ---
(r'\bactivar\b', 'activar'), # PT-PT mantém 'c'
(r'\bActivar\b', 'Activar'),
(r'\bdesactivar\b', 'desactivar'),
(r'\bDesactivar\b', 'Desactivar'),
(r'\bactividade\b', 'actividade'),
(r'\bActividade\b', 'Actividade'),
(r'\bactividades\b', 'actividades'),
(r'\bActividades\b', 'Actividades'),
(r'\bactivo\b', 'activo'),
(r'\bActivo\b', 'Activo'),
(r'\bactivos\b', 'activos'),
(r'\bActivos\b', 'Activos'),
(r'\bactual\b', 'actual'),
(r'\bActual\b', 'Actual'),
(r'\bactualizar\b', 'actualizar'),
(r'\bActualizar\b', 'Actualizar'),
(r'\bactualização\b', 'actualização'),
(r'\bActualização\b', 'Actualização'),
(r'\bactualizado\b', 'actualizado'),
(r'\bActualizado\b', 'Actualizado'),
# "ação" → "acção" (PT-PT usa duplo c)
(r'\bImpressão\b', 'Impressão'), # igual
(r'\bacções\b', 'acções'), # já correcto
(r'\bAcções\b', 'Acções'),
(r'\bações\b', 'acções'),
(r'\bAções\b', 'Acções'),
(r'\bacção\b', 'acção'),
(r'\bAcção\b', 'Acção'),
(r'\bação\b', 'acção'),
(r'\bAção\b', 'Acção'),
(r'\binformações\b', 'informações'), # igual
(r'\bnotificações\b', 'notificações'), # igual
(r'\bpermissões\b', 'permissões'), # igual
]
def apply_ptpt_fixes(text):
"""Aplica todas as correcções PT-BR → PT-PT."""
for pattern, replacement in PTBR_TO_PTPT:
text = re.sub(pattern, replacement, text)
return text
def translate_api(text, retries=3):
"""
Traduz texto via LibreTranslate API (en→pt).
Retorna None em caso de erro.
"""
if not text or not text.strip():
return text
payload = json.dumps({
"q": text,
"source": "en",
"target": "pt",
"format": "text"
}).encode('utf-8')
for attempt in range(retries):
try:
req = urllib.request.Request(
TRANSLATE_API,
data=payload,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=20) as resp:
result = json.loads(resp.read().decode('utf-8'))
translated = result.get('translatedText', text)
return translated
except Exception as e:
if attempt < retries - 1:
time.sleep(2)
else:
print(" ERRO API: {} | texto: {!r}".format(e, text[:50]),
file=sys.stderr)
return None
return None
def translate_with_protection(text):
"""
Traduz texto protegendo termos de marca.
Retorna tradução em PT-PT ou None em erro.
"""
if not text or not text.strip():
return text
# 1. Proteger termos
protected, token_map = protect_terms(text)
# 2. Traduzir via API
translated = translate_api(protected)
if translated is None:
return None
# 3. Restaurar termos protegidos
translated = restore_terms(translated, token_map)
# 4. Aplicar correcções PT-BR → PT-PT
translated = apply_ptpt_fixes(translated)
return translated
def escape_po(text):
"""Escapa string para formato .po."""
text = text.replace('\\', '\\\\')
text = text.replace('"', '\\"')
text = text.replace('\n', '\\n')
text = text.replace('\t', '\\t')
return text
def unescape_po(text):
"""Remove escaping de string .po."""
text = text.replace('\\n', '\n')
text = text.replace('\\t', '\t')
text = text.replace('\\"', '"')
text = text.replace('\\\\', '\\')
return text
# =============================================================================
# PROCESSAMENTO DO .PO
# Abordagem line-by-line para máxima compatibilidade
# =============================================================================
def process_po_file(filepath, dry_run=False, fix_only=False, retranslate=False):
"""
Processa um ficheiro .po:
- Traduz strings vazias (msgstr "")
- Corrige PT-BR em msgstr existentes
- Com --retranslate, re-traduz strings suspeitas
Retorna (traduzidas, corrigidas, erros)
"""
print("\n" + "=" * 60)
print("Processando: {}".format(os.path.basename(filepath)))
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
new_lines = []
translated_count = 0
fixed_count = 0
error_count = 0
skipped_header = False
i = 0
while i < len(lines):
line = lines[i]
# --- Detectar msgstr vazio (a traduzir) ---
if line.startswith('msgstr ""') or line == 'msgstr ""':
# Verificar se é o header (msgid "")
k = i - 1
msgid = ''
while k >= 0:
if lines[k].startswith('msgid "'):
msgid_raw = lines[k][7:].strip().strip('"')
m2 = k + 1
while m2 < i and lines[m2].startswith('"') and not lines[m2].startswith('msgstr'):
msgid_raw += lines[m2].strip().strip('"')
m2 += 1
msgid = unescape_po(msgid_raw)
break
elif lines[k].strip() == '' or lines[k].startswith('#'):
break
k -= 1
# Ignorar header (msgid vazio)
if not msgid:
new_lines.append(line)
i += 1
continue
# Verificar se há linhas de continuação (msgstr multi-linha vazio)
j = i + 1
next_empty = True
while j < len(lines) and lines[j].startswith('"'):
if lines[j].strip() not in ('""', '"\\n"', '""'):
if lines[j].strip() != '""':
next_empty = False
break
j += 1
if next_empty and not fix_only:
# Traduzir
translation = translate_with_protection(msgid)
time.sleep(RATE_LIMIT_DELAY)
if translation:
escaped = escape_po(translation)
print(" + {!r:.50}{!r:.50}".format(msgid, translation))
new_lines.append('msgstr "{}"'.format(escaped))
translated_count += 1
i += 1
# Saltar linhas de continuação vazias
while i < len(lines) and lines[i].startswith('"'):
i += 1
continue
else:
error_count += 1
new_lines.append(line)
i += 1
continue
# --- Corrigir PT-BR em msgstr existente (singular) ---
if line.startswith('msgstr "') and line != 'msgstr ""':
fixed = apply_ptpt_fixes(line)
if fixed != line:
fixed_count += 1
new_lines.append(fixed)
i += 1
continue
# --- Corrigir PT-BR em msgstr plural: msgstr[0], msgstr[1], etc. ---
if re.match(r'^msgstr\[\d+\] "', line):
fixed = apply_ptpt_fixes(line)
if fixed != line:
fixed_count += 1
new_lines.append(fixed)
i += 1
continue
# --- Corrigir continuações de msgstr ---
if (line.startswith('"') and i > 0 and
new_lines and
(new_lines[-1].startswith('msgstr') or
(new_lines[-1].startswith('"') and
len(new_lines) > 1 and new_lines[-2].startswith('msgstr')))):
# Verificar que não é msgid
in_msgstr = False
for prev in reversed(new_lines[-10:] if len(new_lines) >= 10 else new_lines):
if prev.startswith('msgstr'):
in_msgstr = True
break
if prev.startswith('msgid'):
break
if in_msgstr:
fixed = apply_ptpt_fixes(line)
if fixed != line:
fixed_count += 1
new_lines.append(fixed)
i += 1
continue
new_lines.append(line)
i += 1
new_content = '\n'.join(new_lines)
print(" Traduzidas: {} strings".format(translated_count))
print(" Corrigidas (PT-BR→PT-PT): {} ocorrências".format(fixed_count))
print(" Erros API: {}".format(error_count))
if not dry_run and (translated_count > 0 or fixed_count > 0):
# Backup
backup = filepath + '.bak'
shutil.copy2(filepath, backup)
print(" Backup: {}".format(os.path.basename(backup)))
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_content)
# Recompilar .mo
mo_path = filepath.replace('.po', '.mo')
result = subprocess.run(
['msgfmt', filepath, '-o', mo_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
if result.returncode == 0:
print(" .mo recompilado: {}".format(os.path.basename(mo_path)))
else:
err = result.stderr
if isinstance(err, bytes):
err = err.decode('utf-8', errors='replace')
print(" AVISO .mo: {}".format(err[:100].strip()))
elif dry_run:
print(" [DRY-RUN: sem alterações gravadas]")
return translated_count, fixed_count, error_count
def main():
args = sys.argv[1:]
dry_run = '--dry-run' in args
fix_only = '--fix-only' in args
retranslate = '--retranslate' in args
files = [a for a in args if not a.startswith('--')]
if not files:
print("Uso: python3 translate-po-v2.py [--dry-run] [--fix-only] <ficheiro.po> [...]")
print()
print("Opções:")
print(" --dry-run Mostra alterações sem gravar")
print(" --fix-only Só corrige PT-BR→PT-PT (sem API)")
print(" --retranslate Re-traduz strings existentes suspeitas")
sys.exit(1)
total_translated = 0
total_fixed = 0
total_errors = 0
for filepath in files:
if not os.path.exists(filepath):
print("Ficheiro não encontrado: {}".format(filepath))
continue
t, f, e = process_po_file(filepath, dry_run, fix_only, retranslate)
total_translated += t
total_fixed += f
total_errors += e
print("\n" + "=" * 60)
print("TOTAL: {} traduzidas | {} corrigidas | {} erros".format(
total_translated, total_fixed, total_errors))
if __name__ == '__main__':
main()