#!/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] [...] 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] [...]") 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()