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,285 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
translate_missing.py — Preenche strings em falta em ficheiros .po via DeepL (PT-PT).
Traduz apenas msgstr vazios (msgstr ""). Não sobrescreve traduções existentes.
Preserva placeholders WordPress (%s, %d, %1$s, {var}, ##var##) e HTML.
Recompila .mo automaticamente após tradução via polib.
Suporta batching (50 strings/pedido) — muito mais rápido que LibreTranslate.
Uso:
python3 translate_missing.py plugin-pt_PT.po [plugin2-pt_PT.po ...]
python3 translate_missing.py plugin-pt_PT.po --dry-run
find /path/to/library -name "*-pt_PT.po" | xargs python3 translate_missing.py
Author: Descomplicar®
Version: 2.1.0 — DeepL PT-PT + suporte plural (msgstr[0]/msgstr[1])
"""
import sys
import re
import time
import shutil
import os
from pathlib import Path
# Carregar .env do mesmo directório
_env_file = Path(__file__).parent / '.env'
if _env_file.exists():
for _line in _env_file.read_text().splitlines():
if '=' in _line and not _line.startswith('#'):
_k, _v = _line.split('=', 1)
os.environ.setdefault(_k.strip(), _v.strip())
try:
import deepl
except ImportError:
print("ERRO: biblioteca 'deepl' não instalada. Correr: pip3 install deepl", file=sys.stderr)
sys.exit(1)
try:
import polib
except ImportError:
print("ERRO: biblioteca 'polib' não instalada. Correr: pip3 install polib", file=sys.stderr)
sys.exit(1)
# Configuração
DEEPL_AUTH_KEY = os.environ.get('DEEPL_API_KEY', '')
DEEPL_GLOSSARY_ID = os.environ.get('DEEPL_GLOSSARY_ID', '')
TARGET_LANG = 'PT-PT'
BATCH_SIZE = 50 # DeepL suporta até 50 textos por pedido
RATE_LIMIT = 0.1 # segundos entre pedidos (muito mais rápido que LibreTranslate)
MAX_RETRIES = 3
RETRY_DELAY = 3
# Padrão para placeholder WordPress — proteger durante tradução
PH_PATTERN = re.compile(
r'(%(?:\d+\$)?[sdfu]|%\.\d+[fF]|\{[^}]+\}|##[^#]+##|<[^>]+>|&[a-z]+;|&#\d+;)',
re.UNICODE
)
def protect_placeholders(text: str) -> tuple[str, list[str]]:
"""
Substitui placeholders por tokens ⟦0⟧, ⟦1⟧, etc. (Unicode, não XML).
DeepL preserva estes tokens sem necessitar de tag_handling.
Devolve (texto_protegido, lista_de_placeholders).
"""
placeholders = []
def replace(m):
token = f'{len(placeholders)}'
placeholders.append(m.group(0))
return token
protected = PH_PATTERN.sub(replace, text)
return protected, placeholders
def restore_placeholders(text: str, placeholders: list[str]) -> str:
"""Restaura placeholders após tradução."""
for i, ph in enumerate(placeholders):
text = text.replace(f'{i}', ph)
return text
def escape_po(text: str) -> str:
"""Escapa caracteres especiais para formato .po."""
text = text.replace('\\', '\\\\')
text = text.replace('"', '\\"')
text = text.replace('\n', '\\n')
text = text.replace('\t', '\\t')
return text
def translate_batch(translator: 'deepl.Translator', texts: list[str]) -> list[str | None]:
"""
Traduz uma lista de textos via DeepL com protecção de placeholders.
Devolve lista de traduções (None em caso de falha individual).
"""
if not texts:
return []
# Proteger placeholders em todos os textos
protected_texts = []
all_placeholders = []
for text in texts:
protected, phs = protect_placeholders(text)
protected_texts.append(protected)
all_placeholders.append(phs)
for attempt in range(MAX_RETRIES):
try:
kwargs = dict(
source_lang='EN',
target_lang=TARGET_LANG,
preserve_formatting=True,
)
if DEEPL_GLOSSARY_ID:
kwargs['glossary'] = DEEPL_GLOSSARY_ID
results = translator.translate_text(protected_texts, **kwargs)
translations = []
for i, result in enumerate(results):
restored = restore_placeholders(result.text, all_placeholders[i])
translations.append(restored)
return translations
except deepl.exceptions.QuotaExceededException:
print("\n ATENÇÃO: Quota DeepL esgotada! Limite mensal atingido.", file=sys.stderr)
return [None] * len(texts)
except Exception as e:
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
else:
print(f" ERRO API DeepL: {e}", file=sys.stderr)
return [None] * len(texts)
return [None] * len(texts)
def process_file(filepath: str, translator: 'deepl.Translator', dry_run: bool = False) -> tuple[int, int]:
"""
Processa um ficheiro .po e traduz strings em falta via DeepL.
Suporta entradas simples (msgstr) e plurais (msgstr[0]/msgstr[1]).
Devolve (strings traduzidas, erros).
"""
path = Path(filepath)
if not path.exists():
print(f" ERRO: ficheiro não encontrado: {filepath}", file=sys.stderr)
return 0, 1
print(f"\n{'='*60}")
print(f" {path.parent.name}/{path.name}")
po = polib.pofile(filepath, encoding='utf-8')
# Recolher entradas simples vazias
# item: ('simple', entry, None)
# item: ('plural_s', entry, None) — singular do plural
# item: ('plural_p', entry, None) — plural do plural
to_translate = [] # (tipo, entry, texto_a_traduzir)
for entry in po:
if entry.msgid_plural:
# Entrada plural — verificar cada forma
s_empty = not entry.msgstr_plural.get(0, '').strip()
p_empty = not entry.msgstr_plural.get(1, '').strip()
if s_empty and entry.msgid.strip():
to_translate.append(('plural_s', entry, entry.msgid))
if p_empty and entry.msgid_plural.strip():
to_translate.append(('plural_p', entry, entry.msgid_plural))
else:
# Entrada simples
if not entry.msgstr.strip() and entry.msgid.strip():
to_translate.append(('simple', entry, entry.msgid))
if not to_translate:
print(f" Sem strings em falta — a saltar")
return 0, 0
n_simple = sum(1 for t, _, _ in to_translate if t == 'simple')
n_plural = sum(1 for t, _, _ in to_translate if t != 'simple')
print(f" {len(to_translate)} strings em falta ({n_simple} simples, {n_plural} plurais) — DeepL PT-PT...")
if dry_run:
print(f" [DRY-RUN] sem alterações gravadas")
return len(to_translate), 0
# Traduzir em batches
translated_count = 0
errors = 0
texts = [texto for _, _, texto in to_translate]
for batch_start in range(0, len(texts), BATCH_SIZE):
batch_items = to_translate[batch_start:batch_start + BATCH_SIZE]
batch_texts = [texto for _, _, texto in batch_items]
results = translate_batch(translator, batch_texts)
time.sleep(RATE_LIMIT)
for (tipo, entry, _), translation in zip(batch_items, results):
if translation:
if tipo == 'simple':
entry.msgstr = translation
elif tipo == 'plural_s':
entry.msgstr_plural[0] = translation
elif tipo == 'plural_p':
entry.msgstr_plural[1] = translation
translated_count += 1
else:
errors += 1
done = min(batch_start + BATCH_SIZE, len(texts))
print(f" [{done}/{len(texts)}] batch OK")
if translated_count == 0:
print(f" Nenhuma string traduzida")
return 0, errors
shutil.copy2(filepath, filepath + '.bak_translate')
po.save(filepath)
# Compilar .mo
mo_path = filepath.replace('.po', '.mo')
try:
po.save_as_mofile(mo_path)
print(f" .mo compilado: OK ({len(po.translated_entries())} strings traduzidas)")
except Exception as e:
print(f" ERRO .mo: {e}", file=sys.stderr)
errors += 1
print(f" Traduzidas: {translated_count} | Erros: {errors}")
return translated_count, errors
def main():
if not DEEPL_AUTH_KEY:
print("ERRO: DEEPL_API_KEY não definida. Criar .env com DEEPL_API_KEY=...", file=sys.stderr)
sys.exit(1)
args = sys.argv[1:]
dry_run = '--dry-run' in args
files = [a for a in args if not a.startswith('--')]
if not files:
print("Uso: python3 translate_missing.py <ficheiro.po> [...] [--dry-run]")
sys.exit(1)
# Inicializar DeepL e verificar quota
try:
translator = deepl.Translator(DEEPL_AUTH_KEY)
usage = translator.get_usage()
if usage.character.valid:
used = usage.character.count
limit = usage.character.limit
remaining = limit - used
print(f"\nDeepL API — Quota: {used:,}/{limit:,} chars usados | {remaining:,} restantes")
if remaining < 10000:
print(" ATENÇÃO: quota quase esgotada!", file=sys.stderr)
if DEEPL_GLOSSARY_ID:
print(f"Glossário activo: {DEEPL_GLOSSARY_ID}")
else:
print("Glossário: não configurado (definir DEEPL_GLOSSARY_ID em .env)")
except Exception as e:
print(f"ERRO ao ligar à API DeepL: {e}", file=sys.stderr)
sys.exit(1)
total_translated = 0
total_errors = 0
for f in files:
t, e = process_file(f, translator, dry_run)
total_translated += t
total_errors += e
# Quota final
try:
usage = translator.get_usage()
if usage.character.valid:
print(f"\nQuota usada nesta sessão: {usage.character.count:,}/{usage.character.limit:,} chars")
except Exception:
pass
print(f"\n{'='*60}")
print(f"TOTAL: {total_translated} traduzidas | {total_errors} erros | {len(files)} ficheiro(s)")
if __name__ == '__main__':
main()