feat(i18n): Simplified Chinese (zh-CN), search answers per locale, fr/de/es data fixes
CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s
CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae61416468
commit
5a9fe676fc
61 files changed
+2517
-920
No files matched your search
+79
-24
@@ -1,4 +1,5 @@
|
||||
import { TOPICOS, normalizar } from "./searchTopics.js";
|
||||
import { BUSCA_I18N } from "./searchI18n.js";
|
||||
|
||||
/**
|
||||
* Motor da busca inteligente (puro: sem React nem DOM).
|
||||
@@ -21,6 +22,26 @@ const INTENCAO_DADOS = new Set([
|
||||
"quanto", "quantos", "quantas", "capacidade", "limite", "limites", "tem", "possui", "suporta",
|
||||
]);
|
||||
|
||||
const CJK = /[\u3400-\u9fff]/;
|
||||
const INTENCAO_PINOS_ZH = ["引脚", "管脚", "针脚", "脚位"];
|
||||
const INTENCAO_DADOS_ZH = ["规格", "参数", "容量", "多少", "支持吗", "是否支持", "有吗"];
|
||||
const STOP_ZH = new Set(["的", "吗", "呢", "啊", "吧", "了", "是", "什么", "哪个", "哪些", "怎么", "如何", "有", "和", "与", "在", "要", "想", "我", "请", "能", "可以"]);
|
||||
|
||||
/** Palavras de um trecho em chinês (Intl.Segmenter; sem ele, bigramas de caracteres). */
|
||||
function segmentar(texto) {
|
||||
let palavras = [];
|
||||
try {
|
||||
const seg = new Intl.Segmenter("zh", { granularity: "word" });
|
||||
palavras = [...seg.segment(texto)].filter((s) => s.isWordLike).map((s) => s.segment);
|
||||
} catch {
|
||||
const cs = [...texto];
|
||||
palavras = cs.length <= 2 ? [texto] : cs.slice(0, -1).map((c, i) => c + cs[i + 1]);
|
||||
}
|
||||
return palavras.filter((w) => !STOP_ZH.has(w));
|
||||
}
|
||||
|
||||
const fmt = (modelo, v) => modelo.replace(/\{(\w+)\}/g, (_, k) => String(v[k] ?? ""));
|
||||
|
||||
const EXEMPLOS_TOPICOS = ["flash", "psram", "usb", "adc", "i2c", "deep sleep", "wifi", "strapping"];
|
||||
|
||||
/** Distância de edição com corte: devolve > max assim que passa do limite. */
|
||||
@@ -45,7 +66,7 @@ const palavrasDe = (texto) => texto.split(/[^a-z0-9]+/).filter((w) => w.length >
|
||||
|
||||
/** Normaliza o payload do servidor uma única vez, na abertura da busca. */
|
||||
export function preparar(dados) {
|
||||
if (!dados) return { itens: [], series: {}, porFuncao: new Map(), aliases: new Map(), chaves: [] };
|
||||
if (!dados) return { itens: [], series: {}, porFuncao: new Map(), aliases: new Map(), chaves: [], locale: "pt", topicosLoc: null };
|
||||
|
||||
const itensRaw = Array.isArray(dados) ? dados : (dados.itens ?? []);
|
||||
const seriesRaw = Array.isArray(dados) ? {} : (dados.series ?? {});
|
||||
@@ -90,11 +111,15 @@ export function preparar(dados) {
|
||||
if (k.startsWith("esp32-")) aliases.set(k.slice(6), chave);
|
||||
}
|
||||
|
||||
return { itens, series, porFuncao, aliases, chaves: Object.keys(series) };
|
||||
return { itens, series, porFuncao, aliases, chaves: Object.keys(series), locale: Array.isArray(dados) ? "pt" : (dados.locale ?? "pt"), topicosLoc: Array.isArray(dados) ? null : (dados.topicos ?? null) };
|
||||
}
|
||||
|
||||
/** Tenta casar um termo do dicionário com os tokens livres. Devolve { idx[], qualidade } ou null. */
|
||||
function casarTermo(termo, livres) {
|
||||
if (CJK.test(termo)) {
|
||||
for (const { t, i } of livres) if (CJK.test(t) && t.includes(termo)) return { idx: [i], qualidade: 3 + termo.length / 10, cjk: true };
|
||||
return null;
|
||||
}
|
||||
const partes = termo.split(" ");
|
||||
if (partes.length > 1) {
|
||||
for (let i = 0; i + partes.length <= livres.length; i++) {
|
||||
@@ -115,7 +140,9 @@ function casarTermo(termo, livres) {
|
||||
export function interpretar(indice, consulta) {
|
||||
const bruto = normalizar(consulta)
|
||||
.replace(/gpio\s*-?\s*(\d+)/g, "gpio$1")
|
||||
.replace(/[^a-z0-9.+\-_ ]/g, " ");
|
||||
.replace(/([\u3400-\u9fff])([a-z0-9])/g, "$1 $2")
|
||||
.replace(/([a-z0-9])([\u3400-\u9fff])/g, "$1 $2")
|
||||
.replace(/[^a-z0-9.+\-_ \u3400-\u9fff]/g, " ");
|
||||
const tokens = bruto.split(/\s+/).filter(Boolean);
|
||||
const usados = new Set();
|
||||
|
||||
@@ -138,6 +165,17 @@ export function interpretar(indice, consulta) {
|
||||
const gpios = [];
|
||||
let querPinos = false;
|
||||
let querDados = false;
|
||||
// Chinês não separa palavras por espaço: tira do trecho as palavras de intenção.
|
||||
tokens.forEach((t, i) => {
|
||||
if (!CJK.test(t)) return;
|
||||
let resto = t;
|
||||
for (const w of INTENCAO_PINOS_ZH) if (resto.includes(w)) { querPinos = true; resto = resto.split(w).join(""); }
|
||||
for (const w of INTENCAO_DADOS_ZH) if (resto.includes(w)) { querDados = true; resto = resto.split(w).join(""); }
|
||||
if (resto !== t) {
|
||||
tokens[i] = resto;
|
||||
if (!resto) usados.add(i);
|
||||
}
|
||||
});
|
||||
tokens.forEach((t, i) => {
|
||||
if (/^gpio\d+$/.test(t)) {
|
||||
gpios.push(t);
|
||||
@@ -158,7 +196,8 @@ export function interpretar(indice, consulta) {
|
||||
const candidatos = [];
|
||||
for (const topico of TOPICOS) {
|
||||
let melhor = null;
|
||||
for (const termo of topico.termos) {
|
||||
const extras = indice.topicosLoc?.[topico.id]?.termos;
|
||||
for (const termo of extras ? [...topico.termos, ...extras] : topico.termos) {
|
||||
const m = casarTermo(normalizar(termo), livres);
|
||||
if (m && (!melhor || m.qualidade > melhor.qualidade)) melhor = m;
|
||||
}
|
||||
@@ -166,8 +205,15 @@ export function interpretar(indice, consulta) {
|
||||
}
|
||||
candidatos.sort((a, b) => b.qualidade - a.qualidade);
|
||||
const topicos = [];
|
||||
const cjkUsados = [];
|
||||
for (const c of candidatos) {
|
||||
if (topicos.length >= 3) break;
|
||||
if (c.cjk) {
|
||||
// Um mesmo trecho em chinês pode citar vários assuntos.
|
||||
topicos.push(c.topico);
|
||||
cjkUsados.push(...c.idx);
|
||||
continue;
|
||||
}
|
||||
if (c.idx.some((i) => usados.has(i))) continue;
|
||||
c.idx.forEach((i) => usados.add(i));
|
||||
topicos.push(c.topico);
|
||||
@@ -183,7 +229,8 @@ export function interpretar(indice, consulta) {
|
||||
}
|
||||
});
|
||||
|
||||
const restantes = tokens.filter((_, i) => !usados.has(i));
|
||||
cjkUsados.forEach((i) => usados.add(i));
|
||||
const restantes = tokens.filter((_, i) => !usados.has(i)).flatMap((t) => (CJK.test(t) ? segmentar(t) : [t]));
|
||||
return { tokens, chips, topicos, gpios, funcoes, restantes, querPinos, querDados };
|
||||
}
|
||||
|
||||
@@ -206,11 +253,17 @@ function pinosDoTopico(serie, topico) {
|
||||
return { forte, fraco };
|
||||
}
|
||||
|
||||
const hrefPinos = (chave, pinos, motivo) =>
|
||||
`/series/${chave}?pinos=${pinos.map((p) => p.n).join(",")}&motivo=${encodeURIComponent(motivo)}#connections`;
|
||||
const hrefPinos = (I, chave, pinos, motivo) =>
|
||||
`${I.rotas.series}/${chave}?pinos=${pinos.map((p) => p.n).join(",")}&motivo=${encodeURIComponent(motivo)}#connections`;
|
||||
|
||||
function respostas(indice, it) {
|
||||
const saida = [];
|
||||
const I = BUSCA_I18N[indice.locale] ?? BUSCA_I18N.pt;
|
||||
const T = I.t;
|
||||
const loc = (topico) => ({
|
||||
nome: indice.topicosLoc?.[topico.id]?.nome ?? topico.nome,
|
||||
rotulo: indice.topicosLoc?.[topico.id]?.pinoRotulo ?? topico.pinoRotulo,
|
||||
});
|
||||
const alvos = it.chips.slice(0, 2);
|
||||
|
||||
for (const chave of alvos) {
|
||||
@@ -218,14 +271,15 @@ function respostas(indice, it) {
|
||||
if (!serie) continue;
|
||||
|
||||
for (const topico of it.topicos.slice(0, 2)) {
|
||||
const tn = loc(topico);
|
||||
const specs = serie.specs.filter((s) => s.topicos.includes(topico.id));
|
||||
const cartaoSpec = specs.length
|
||||
? {
|
||||
tipo: "resposta",
|
||||
titulo: `${topico.nome} no ${serie.nome}`,
|
||||
descricao: "Dados da série",
|
||||
titulo: fmt(T.specTitulo, { topico: tn.nome, serie: serie.nome }),
|
||||
descricao: T.specDesc,
|
||||
linhas: specs.slice(0, 5).map((s) => ({ k: s.label, v: s.valor })),
|
||||
href: `/series/${chave}#${specs[0].secao}`,
|
||||
href: `${I.rotas.series}/${chave}#${specs[0].secao}`,
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -234,19 +288,19 @@ function respostas(indice, it) {
|
||||
const cartaoPinos = principais.length
|
||||
? {
|
||||
tipo: "pino",
|
||||
titulo: `Pinos do ${serie.nome} ${topico.pinoRotulo}`,
|
||||
titulo: fmt(T.pinosTitulo, { serie: serie.nome, rotulo: tn.rotulo }),
|
||||
descricao:
|
||||
forte.length && fraco.length
|
||||
? `${forte.length} ocupados + ${fraco.length} que dependem do modo de uso (ex.: SPI de 8 linhas) · abre o diagrama filtrado`
|
||||
? fmt(T.pinosMistos, { forte: forte.length, fraco: fraco.length })
|
||||
: forte.length
|
||||
? `${forte.length} ${forte.length === 1 ? "pino" : "pinos"} · abre o diagrama já filtrado`
|
||||
: `${fraco.length} pinos que só são afetados em certos modos de uso · abre o diagrama filtrado`,
|
||||
? fmt(forte.length === 1 ? T.pinoUm : T.pinosVarios, { n: forte.length })
|
||||
: fmt(T.pinosFraco, { n: fraco.length }),
|
||||
chips: forte
|
||||
.slice(0, 12)
|
||||
.map(rotuloPino)
|
||||
.concat(forte.length > 12 ? [`+${forte.length - 12}`] : [])
|
||||
.concat(fraco.length && forte.length ? [`+${fraco.length} condicionais`] : forte.length ? [] : fraco.slice(0, 10).map(rotuloPino)),
|
||||
href: hrefPinos(chave, [...forte, ...fraco], `${topico.nome}: pinos ${topico.pinoRotulo}`),
|
||||
.concat(fraco.length && forte.length ? [fmt(T.condicionais, { n: fraco.length })] : forte.length ? [] : fraco.slice(0, 10).map(rotuloPino)),
|
||||
href: hrefPinos(I, chave, [...forte, ...fraco], fmt(T.motivoTopico, { topico: tn.nome, rotulo: tn.rotulo })),
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -257,13 +311,13 @@ function respostas(indice, it) {
|
||||
if (it.querPinos && serie.pinos.length) {
|
||||
saida.push({
|
||||
tipo: "pino",
|
||||
titulo: `Diagrama de pinos do ${serie.nome}`,
|
||||
descricao: `${serie.pinos.length} pinos com funções, restrições e avisos`,
|
||||
href: `/series/${chave}#connections`,
|
||||
titulo: fmt(T.diagTitulo, { serie: serie.nome }),
|
||||
descricao: fmt(T.diagDesc, { n: serie.pinos.length }),
|
||||
href: `${I.rotas.series}/${chave}#connections`,
|
||||
});
|
||||
}
|
||||
if (it.querDados) {
|
||||
saida.push({ tipo: "resposta", titulo: `Especificações do ${serie.nome}`, descricao: "Processador, memória, conectividade, periféricos e mais", href: `/series/${chave}` });
|
||||
saida.push({ tipo: "resposta", titulo: fmt(T.especTitulo, { serie: serie.nome }), descricao: T.especDesc, href: `${I.rotas.series}/${chave}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,6 +325,7 @@ function respostas(indice, it) {
|
||||
// Assunto sem chip: panorama por série ("qual série tem PSRAM?")
|
||||
if (!alvos.length && it.topicos.length) {
|
||||
const topico = it.topicos[0];
|
||||
const tn = loc(topico);
|
||||
const linhas = [];
|
||||
for (const chave of indice.chaves) {
|
||||
const serie = indice.series[chave];
|
||||
@@ -278,7 +333,7 @@ function respostas(indice, it) {
|
||||
if (spec) linhas.push({ k: serie.nome, v: spec.valor.length > 46 ? `${spec.valor.slice(0, 45)}…` : spec.valor });
|
||||
}
|
||||
if (linhas.length > 1) {
|
||||
saida.push({ tipo: "resposta", titulo: `${topico.nome} em cada série`, descricao: "Comparativo rápido · adicione o chip na busca para detalhar", linhas, href: "/comparacao" });
|
||||
saida.push({ tipo: "resposta", titulo: fmt(T.emCada, { topico: tn.nome }), descricao: T.compDesc, linhas, href: I.rotas.comparacao });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,9 +346,9 @@ function respostas(indice, it) {
|
||||
const serie = indice.series[chave];
|
||||
saida.push({
|
||||
tipo: "pino",
|
||||
titulo: `${rotuloPino(pino)} no ${serie.nome}`,
|
||||
titulo: fmt(T.pinoNo, { pino: rotuloPino(pino), serie: serie.nome }),
|
||||
descricao: [pino.f.join(" · "), pino.a[0]].filter(Boolean).join(" — "),
|
||||
href: hrefPinos(chave, [pino], `Pino ${rotuloPino(pino)}`),
|
||||
href: hrefPinos(I, chave, [pino], fmt(T.motivoPino, { pino: rotuloPino(pino) })),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -331,7 +386,7 @@ export function buscar(indice, consulta) {
|
||||
const it = interpretar(indice, consulta);
|
||||
const interpretacao = {
|
||||
chips: it.chips.map((c) => ({ chave: c, nome: indice.series[c]?.nome ?? c })),
|
||||
topicos: it.topicos.map((t) => ({ id: t.id, nome: t.nome })),
|
||||
topicos: it.topicos.map((t) => ({ id: t.id, nome: indice.topicosLoc?.[t.id]?.nome ?? t.nome })),
|
||||
};
|
||||
|
||||
const smart = respostas(indice, it).map((r, i) => ({ id: `r${i}`, keywords: "", ...r }));
|
||||
|
||||
Reference in new issue
Block a user