- Logo oficial (sabia.png) e conjunto de ícones do app desktop. - Interface em português e inglês: tradutor por observação do DOM, seletor de idioma e checagem de cobertura de tradução no CI. - CI/CD com caches (rust-cache, .deb do Tauri em cache) e pipeline de release multiplataforma (CLI em 5 alvos + app desktop via tauri-action), templates de issue/PR. - Suíte de testes de integração do sabia-core (scan e2e, handshake/crack 4-way e PMKID, pcap, trilha de auditoria) — 46 testes verdes. - Documentação completa pt/en: README, ROADMAP, SECURITY, CONTRIBUTING, CHANGELOG e docs/ (cli, interface, arquitetura), com enquadramento de uso autorizado. - Correções de clippy (-D warnings) e formatação em toda a árvore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
4.3 KiB
JavaScript
123 lines
4.3 KiB
JavaScript
// Idioma da interface. O texto-fonte é em português; no modo inglês um tradutor
|
|
// troca os textos exibidos (nós de texto e atributos) usando um dicionário, e
|
|
// continua observando a página para traduzir o que for desenhado depois.
|
|
// Textos com valores variáveis usam {n} no dicionário (ex.: "{n} redes" -> "{n} networks").
|
|
|
|
import { EN } from './i18n-en.js';
|
|
|
|
let lang = 'pt';
|
|
export const isEn = () => lang === 'en';
|
|
export const currentLang = () => lang;
|
|
export const LOC = () => (lang === 'en' ? 'en-US' : 'pt-BR');
|
|
|
|
export function resolveLang(pref) {
|
|
if (pref === 'pt' || pref === 'en') return pref;
|
|
return (navigator.language || 'pt').toLowerCase().startsWith('pt') ? 'pt' : 'en';
|
|
}
|
|
|
|
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
|
// Dados do usuário (SSID, BSSID, MAC, valores mono) nunca são traduzidos.
|
|
const SKIP = '.ssid, .bssid, .mono, pre, code, input, textarea, script, style';
|
|
|
|
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
|
|
const patterns = Object.entries(EN)
|
|
.filter(([k]) => k.includes('{n}'))
|
|
.map(([k, v]) => {
|
|
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v };
|
|
})
|
|
// padrões mais específicos (mais texto fixo) primeiro
|
|
.sort((a, b) => b.re.source.length - a.re.source.length);
|
|
|
|
const cache = new Map();
|
|
export function tr(text) {
|
|
if (!isEn()) return text;
|
|
const lead = text.match(/^\s*/)[0];
|
|
const trail = text.match(/\s*$/)[0];
|
|
const core = text.trim();
|
|
if (!core) return text;
|
|
if (cache.has(core)) return lead + cache.get(core) + trail;
|
|
let out = exact.get(core);
|
|
if (out === undefined) {
|
|
for (const p of patterns) {
|
|
const m = core.match(p.re);
|
|
if (m) {
|
|
let i = 0;
|
|
// os valores capturados também podem ter partes traduzíveis (ex.: estados)
|
|
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (out === undefined) out = core;
|
|
if (cache.size > 5000) cache.clear();
|
|
cache.set(core, out);
|
|
return lead + out + trail;
|
|
}
|
|
|
|
const orig = new WeakMap(); // nó -> texto original em português
|
|
let observer = null;
|
|
|
|
function translateNode(n) {
|
|
if (n.nodeType === 3) {
|
|
if (n.parentElement?.closest(SKIP)) return;
|
|
const o = orig.get(n) ?? n.nodeValue;
|
|
const t = tr(o);
|
|
if (t !== n.nodeValue) {
|
|
if (!orig.has(n)) orig.set(n, o);
|
|
n.nodeValue = t;
|
|
}
|
|
} else if (n.nodeType === 1) {
|
|
if (n.matches?.(SKIP) && n.tagName !== 'INPUT') return;
|
|
for (const a of ATTRS) {
|
|
if (n.hasAttribute?.(a)) {
|
|
const key = `i18n-${a}`;
|
|
const o = n[key] ?? n.getAttribute(a);
|
|
const t = tr(o);
|
|
if (t !== n.getAttribute(a)) { n[key] = o; n.setAttribute(a, t); }
|
|
}
|
|
}
|
|
for (const c of n.childNodes) translateNode(c);
|
|
}
|
|
}
|
|
|
|
function restoreAll(root) {
|
|
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
let n;
|
|
while ((n = w.nextNode())) { if (orig.has(n)) { n.nodeValue = orig.get(n); orig.delete(n); } }
|
|
root.querySelectorAll('*').forEach((el) => {
|
|
for (const a of ATTRS) { const key = `i18n-${a}`; if (el[key] !== undefined) { el.setAttribute(a, el[key]); delete el[key]; } }
|
|
});
|
|
}
|
|
|
|
function start() {
|
|
translateNode(document.body);
|
|
document.title = tr(document.title);
|
|
if (observer) return;
|
|
observer = new MutationObserver((muts) => {
|
|
observer.disconnect();
|
|
for (const m of muts) {
|
|
if (m.type === 'childList') m.addedNodes.forEach(translateNode);
|
|
else if (m.type === 'characterData') { orig.delete(m.target); translateNode(m.target); }
|
|
else if (m.type === 'attributes') { delete m.target[`i18n-${m.attributeName}`]; translateNode(m.target); }
|
|
}
|
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ATTRS });
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ATTRS });
|
|
}
|
|
|
|
function stop() {
|
|
observer?.disconnect();
|
|
observer = null;
|
|
restoreAll(document.body);
|
|
}
|
|
|
|
export function setLang(pref) {
|
|
const next = resolveLang(pref);
|
|
const changed = next !== lang;
|
|
lang = next;
|
|
document.documentElement.lang = lang === 'en' ? 'en' : 'pt-BR';
|
|
if (lang === 'en') start();
|
|
else if (changed || observer) stop();
|
|
}
|