1052 lines
32 KiB
Python
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()
|