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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-11-01 23:28:39 -03:00
1 parent ae61416468
commit 5a9fe676fc
61 files changed
+2517 -920

No files matched your search

+1 -1
View File
@@ -166,7 +166,7 @@ export function montarConhecimento(series, conexoes) {
for (const [modo, valor] of Object.entries(s.consumo_energia ?? {})) {
const base = modo.replace(/_cpu.*|_wifi.*/, "");
const label = ROTULOS_ENERGIA[modo] ?? ROTULOS_ENERGIA[base] ?? modo.replace(/_/g, " ");
specs.push({ label: `Consumo: ${label}`, valor: String(valor), secao: "energia", topicos: ["consumo"] });
specs.push({ label: `Power: ${label}`, valor: String(valor), secao: "energia", topicos: ["consumo"] });
}
saida[chave] = {
nome: s.nome_completo ?? chave,
+5 -5
View File
@@ -278,7 +278,7 @@ export const COMPONENTES_REGISTRY = [
categoria: "cloud_conectividade",
oficial: true,
popular: true,
titulo: "ESP-NOW Enhanced & Provisioning",
titulo: "ESP-NOW 增强版与配网",
descricao: "ESP-NOW 协议的扩展,提供遥控配对、AES-128 安全以及多设备控制等高级功能。",
tags: ["ESP-NOW", "无线", "低延迟", "广播", "遥控"],
chips: ["ESP32", "ESP32-S2", "ESP32-S3", "ESP32-C3", "ESP32-C6"],
@@ -406,7 +406,7 @@ export const COMPONENTES_REGISTRY = [
oficial: true,
popular: true,
titulo: "ESP Matter SDK",
descricao: "官方 SDK,用于创建可在 Wi-Fi 和 Thread 上与 Apple Home、Google Home、Amazon Alexa 和 Home Assistant 互操作的设备。",
descricao: "官方 SDK,用于创建可在 Wi-Fi 和 Thread 上与 Apple Home、Google Home、Alexa 和 Home Assistant 互操作的设备。",
tags: ["Matter", "智能家居", "Apple HomeKit", "Google Home", "Thread", "CHIP"],
chips: ["ESP32", "ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-H2"],
urlRegistry: "https://components.espressif.com/components/espressif/esp-matter",
@@ -494,7 +494,7 @@ export const COMPONENTES_REGISTRY = [
categoria: "drivers_perifericos",
oficial: true,
popular: true,
titulo: "Addressable LED Strip (WS2812/SK6812)",
titulo: "可寻址 LED 灯带(WS2812/SK6812)",
descricao: "高性能驱动,使用 RMT 或 SPI 外设控制 WS2812、WS2813 和 SK6812 可寻址 LED 灯带与灯阵。",
tags: ["WS2812B", "Neopixel", "SK6812", "RMT", "LED 灯带", "RGB"],
chips: ["ESP32", "ESP32-S2", "ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4"],
@@ -719,7 +719,7 @@ export const COMPONENTES_REGISTRY = [
categoria: "bsp_placas",
oficial: false,
popular: true,
titulo: "BSP Waveshare ESP32-P4 Touch LCD 7\"",
titulo: "BSP Waveshare ESP32-P4 7\" 触摸 LCD",
descricao: "面向高性能 Waveshare ESP32-P4 开发板的支持包,配备 7 英寸电容触摸屏(MIPI-DSI 1024x600)和 Wi-Fi 6 协处理器。",
tags: ["BSP", "ESP32-P4", "MIPI-DSI", "Waveshare", "7 英寸屏", "触摸"],
chips: ["ESP32-P4"],
@@ -757,7 +757,7 @@ export const COMPONENTES_REGISTRY = [
categoria: "sistema_utilitarios",
oficial: true,
popular: true,
titulo: "ESP-IDF C++ Idiomatic Wrappers",
titulo: "ESP-IDF C++ 现代封装",
descricao: "现代 C++(C++17/C++20/C++23)类与封装集合,采用 RAII 语义,覆盖 GPIO、Wi-Fi、定时器、FreeRTOS 队列和信号量。",
tags: ["C++", "C++20", "RAII", "Modern C++", "面向对象", "FreeRTOS"],
chips: ["ESP32", "ESP32-S2", "ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-H2", "ESP32-P4"],
+184
View File
@@ -0,0 +1,184 @@
/**
* Conhecimento de domínio da busca inteligente (zh-CN).
*
* Sem dependências nem alias de import: roda no servidor (montando o índice),
* no navegador (interpretando a consulta) e em scripts de teste no Node.
*
* Todos os textos de `termos` já estão normalizados (minúsculos, sem acento).
*/
export const normalizar = (s = "") =>
String(s)
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
/**
* Reconhece pinos ocupados por flash/PSRAM com base no que o diagrama diz.
* "forte": o pino é consumido pela memória (ou é a alimentação dela).
* "fraco": só é afetado em certos modos (ex.: SPI de 8 linhas/octal).
* Avisos genéricos de tensão mínima ("para chips com flash integrada...") não contam.
*/
const nivelMemoria = (palavra) => (p) => {
const citam = p._avs.filter((a) => a.includes(palavra) && !/^对于集成/.test(a));
if (citam.some((a) => /(存储使用|专用于|连接到|用于存储)/.test(a) && !/除非/.test(a))) return "forte";
if (p._fn.some((f) => /(供电|alimentacao|vdd)/.test(f) && f.includes(palavra))) return "forte";
if (citam.length) return "fraco";
return null;
};
/**
* Assuntos que o usuário pode citar. Cada um sabe:
* - termos: como as pessoas escrevem (sinônimos, siglas, plurais comuns);
* - pinoCats / pinoRe: como reconhecer os pinos relacionados no diagrama;
* - pinoRotulo: frase usada no título do resultado de pinos.
* Os dados de especificação ligam-se aos assuntos por SPEC_CAMPOS.
*/
export const TOPICOS = [
{ id: "flash", nome: "闪存", termos: ["flash", "memoria flash", "spi flash", "armazenamento", "firmware", "rom externa", "闪存", "外部闪存", "程序存储", "固件存储"], pinoNivel: nivelMemoria("flash"), pinoRotulo: "连接到闪存" },
{ id: "psram", nome: "PSRAM", termos: ["psram", "ram externa", "spiram", "外部内存", "伪静态"], pinoNivel: nivelMemoria("psram"), pinoRotulo: "连接到 PSRAM" },
{ id: "memoria", nome: "内存", termos: ["memoria", "sram", "heap", "rtc ram", "kb de ram", "memoria interna", "ram", "memory", "内存", "内部内存", "堆", "存储器", "容量"], pinoRotulo: "" },
{ id: "wifi", nome: "Wi-Fi", termos: ["wifi", "wi-fi", "wireless", "sem fio", "802.11", "wifi6", "wifi 6", "wi-fi 6", "无线网", "无线", "无线网络"], pinoCats: ["Antena"], pinoRotulo: "天线与射频" },
{ id: "bluetooth", nome: "蓝牙", termos: ["bluetooth", "ble", "bt", "bluetooth le", "mesh", "蓝牙", "低功耗蓝牙", "经典蓝牙"], pinoCats: ["Antena"], pinoRotulo: "天线与射频" },
{ id: "zigbee", nome: "Zigbee/Thread", termos: ["zigbee", "thread", "802.15.4", "ieee 802.15.4", "组网"], pinoRotulo: "" },
{ id: "matter", nome: "Matter", termos: ["matter", "casa inteligente", "smart home", "智能家居", "家居"], pinoRotulo: "" },
{ id: "ethernet", nome: "以太网", termos: ["ethernet", "rmii", "cabo de rede", "lan", "以太网", "网口", "有线网络", "网线"], pinoCats: ["Ethernet"], pinoRotulo: "以太网" },
{ id: "can", nome: "CAN / TWAI", termos: ["can", "twai", "can bus", "canbus", "can fd", "总线", "车载总线"], pinoRe: /twai|\bcan\b/, pinoRotulo: "CAN/TWAI" },
{ id: "usb", nome: "USB", termos: ["usb", "otg", "usb serial", "usb otg", "hid", "cdc", "原生usb"], pinoCats: ["USB"], pinoRotulo: "USB" },
{ id: "jtag", nome: "JTAG / 调试", termos: ["jtag", "debug", "depuracao", "depurar", "openocd", "gdb", "调试", "调试器", "断点"], pinoCats: ["JTAG"], pinoRotulo: "JTAG" },
{ id: "adc", nome: "ADC", termos: ["adc", "analogico", "entrada analogica", "conversor analogico", "analogread", "saradc", "analog", "模数转换", "模拟输入", "模拟量", "模拟"], pinoCats: ["ADC"], pinoRotulo: "带 ADC 功能" },
{ id: "dac", nome: "DAC", termos: ["dac", "saida analogica", "conversor digital", "数模转换", "模拟输出"], pinoCats: ["DAC"], pinoRotulo: "带 DAC 功能" },
{ id: "touch", nome: "电容触摸", termos: ["touch", "capacitivo", "toque", "touchpad", "sensor de toque", "触摸", "触控", "电容"], pinoCats: ["Touch"], pinoRotulo: "电容触摸" },
{ id: "uart", nome: "UART", termos: ["uart", "serial", "tx", "rx", "txd", "rxd", "porta serial", "串口", "串行", "串行通信"], pinoCats: ["UART"], pinoRotulo: "UART" },
{ id: "spi", nome: "SPI", termos: ["spi", "mosi", "miso", "sclk", "barramento spi", "spi总线"], pinoCats: ["SPI"], pinoRotulo: "SPI" },
{ id: "i2c", nome: "I2C", termos: ["i2c", "sda", "scl", "twi", "iic", "i2c总线"], pinoCats: ["I2C"], pinoRotulo: "I2C" },
{ id: "i2s", nome: "I2S", termos: ["i2s", "audio", "som", "microfone", "pdm", "codec", "音频", "声音", "麦克风", "编解码"], pinoRe: /i2s|\bpdm\b/, pinoRotulo: "I2S / 音频" },
{ id: "pwm", nome: "PWM", termos: ["pwm", "ledc", "mcpwm", "servo", "dimmer", "脉宽", "舵机", "调光", "电机"], pinoRe: /ledc|pwm/, pinoRotulo: "PWM" },
{ id: "sd", nome: "SD 卡 / SDIO", termos: ["sd", "sdio", "sd card", "sdcard", "cartao sd", "microsd", "sdmmc", "mmc", "sd卡", "存储卡", "内存卡"], pinoCats: ["SD"], pinoRe: /sdio|sdmmc|\bsd\b/, pinoRotulo: "SD 卡 / SDIO" },
{ id: "camera", nome: "摄像头", termos: ["camera", "cam", "dvp", "csi", "mipi csi", "ov2640", "video", "摄像头", "相机", "视频", "拍照"], pinoCats: ["CSI"], pinoRe: /\bcam\b|dvp|cam_/, pinoRotulo: "摄像头" },
{ id: "lcd", nome: "显示屏 / LCD", termos: ["lcd", "display", "tela", "dsi", "mipi dsi", "oled", "tft", "lvgl", "rgb", "显示屏", "屏幕", "液晶", "显示"], pinoCats: ["DSI"], pinoRe: /lcd/, pinoRotulo: "显示屏/LCD" },
{ id: "strapping", nome: "启动配置", termos: ["strapping", "boot", "bootstrap", "modo de boot", "download mode", "modo de gravacao", "inicializacao", "启动配置", "启动模式", "下载模式", "烧录模式"], pinoCats: ["Strapping"], pinoRotulo: "启动配置" },
{ id: "energia_pinos", nome: "电源", termos: ["alimentacao", "vdd", "3v3", "3.3v", "tensao", "regulador", "ldo", "fonte", "power", "电源", "供电", "电压", "稳压", "稳压器"], pinoCats: ["Power"], pinoRotulo: "电源" },
{ id: "clock", nome: "时钟 / 晶振", termos: ["clock", "xtal", "cristal", "oscilador", "32khz", "40mhz", "crystal", "时钟", "晶振", "晶体", "振荡器"], pinoCats: ["Clock"], pinoRotulo: "时钟/晶振" },
{ id: "gpio", nome: "GPIO", termos: ["gpio", "entradas e saidas", "通用io", "输入输出"], pinoCats: ["GPIO"], pinoRotulo: "GPIO" },
{ id: "baixo_consumo", nome: "低功耗 / ULP", termos: ["ulp", "lp core", "lp cpu", "coprocessador", "lp gpio", "low power", "rtc gpio", "协处理器", "超低功耗"], pinoCats: ["Low Power"], pinoRotulo: "低功耗(LP/RTC)" },
{ id: "antena", nome: "天线 / 射频", termos: ["antena", "rf", "radiofrequencia", "ipex", "antenna", "天线", "射频"], pinoCats: ["Antena"], pinoRotulo: "天线/射频" },
{ id: "seguranca", nome: "安全", termos: ["seguranca", "secure boot", "criptografia", "encriptacao", "aes", "sha", "rsa", "hmac", "ecdsa", "efuse", "tee", "trng", "rng", "hsm", "protecao", "security", "安全", "加密", "安全启动", "密钥", "熔丝", "可信"], pinoRotulo: "" },
{ id: "consumo", nome: "功耗", termos: ["consumo", "bateria", "deep sleep", "light sleep", "sleep", "modem sleep", "corrente", "miliamperes", "autonomia", "economia de energia", "low power mode", "power consumption", "功耗", "电池", "深度睡眠", "浅睡眠", "电流", "待机", "续航", "省电", "耗电"], pinoRotulo: "" },
{ id: "temperatura", nome: "温度", termos: ["temperatura", "calor", "graus", "faixa de temperatura", "industrial", "temperature", "温度", "工作温度", "高温", "低温"], pinoRotulo: "" },
{ id: "ia", nome: "AI / 加速", termos: ["ia", "ai", "machine learning", "ml", "tinyml", "vetorial", "inteligencia artificial", "rede neural", "esp-nn", "esp-dl", "人工智能", "机器学习", "神经网络", "边缘ai", "向量"], pinoRotulo: "" },
{ id: "processador", nome: "处理器", termos: ["processador", "cpu", "nucleo", "nucleos", "core", "dual core", "single core", "arquitetura", "xtensa", "risc-v", "riscv", "processor", "处理器", "内核", "核心", "双核", "单核", "架构", "主频"], pinoRotulo: "" },
{ id: "frequencia", nome: "频率", termos: ["frequencia", "mhz", "velocidade", "desempenho", "performance", "frequency", "频率", "主频", "速度", "性能"], pinoRotulo: "" },
{ id: "ferramentas", nome: "工具 / SDK", termos: ["esp-idf", "idf", "arduino", "sdk", "toolchain", "versao do idf", "tools", "开发环境", "工具链", "开发框架"], pinoRotulo: "" },
{ id: "encapsulamento", nome: "模组与封装", termos: ["modulo", "modulos", "encapsulamento", "package", "qfn", "wroom", "wrover", "mini-1", "modules", "模组", "封装", "型号"], pinoRotulo: "" },
];
/**
* Campos de public/series.json que respondem a cada assunto.
* `campo` aceita caminho com ponto (seguranca.aes). `secao` é o id da âncora
* na página /series/[key].
*/
export const SPEC_CAMPOS = [
{ campo: "arquitetura", label: "架构", secao: "processador", topicos: ["processador"] },
{ campo: "nucleos", label: "内核", secao: "processador", topicos: ["processador"] },
{ campo: "frequencia", label: "频率", secao: "processador", topicos: ["frequencia", "processador"] },
{ campo: "coprocessador_ulp", label: "ULP 协处理器", secao: "processador", topicos: ["baixo_consumo"] },
{ campo: "aceleradores_ia", label: "AI 加速器", secao: "processador", topicos: ["ia"] },
{ campo: "wifi", label: "Wi-Fi", secao: "conectividade", topicos: ["wifi"] },
{ campo: "bluetooth", label: "蓝牙", secao: "conectividade", topicos: ["bluetooth"] },
{ campo: "zigbee_thread", label: "Zigbee/Thread", secao: "conectividade", topicos: ["zigbee"] },
{ campo: "matter", label: "Matter", secao: "conectividade", topicos: ["matter"] },
{ campo: "ethernet", label: "以太网", secao: "conectividade", topicos: ["ethernet"] },
{ campo: "ethernet_mac", label: "以太网 MAC", secao: "conectividade", topicos: ["ethernet"] },
{ campo: "can", label: "CAN", secao: "conectividade", topicos: ["can"] },
{ campo: "memoria_sram", label: "SRAM", secao: "memoria", topicos: ["memoria"] },
{ campo: "memoria_sram_rtc", label: "RTC SRAM", secao: "memoria", topicos: ["memoria", "baixo_consumo"] },
{ campo: "memoria_rom", label: "ROM", secao: "memoria", topicos: ["memoria"] },
{ campo: "flash_externa", label: "外部闪存", secao: "memoria", topicos: ["flash", "memoria"] },
{ campo: "psram_externa", label: "外部 PSRAM", secao: "memoria", topicos: ["psram", "memoria"] },
{ campo: "gpio", label: "GPIO", secao: "perifericos", topicos: ["gpio"] },
{ campo: "adc", label: "ADC", secao: "perifericos", topicos: ["adc"] },
{ campo: "dac", label: "DAC", secao: "perifericos", topicos: ["dac"] },
{ campo: "touch", label: "触摸", secao: "perifericos", topicos: ["touch"] },
{ campo: "uart", label: "UART", secao: "perifericos", topicos: ["uart"] },
{ campo: "spi", label: "SPI", secao: "perifericos", topicos: ["spi", "flash"] },
{ campo: "i2c", label: "I2C", secao: "perifericos", topicos: ["i2c"] },
{ campo: "i2s", label: "I2S", secao: "perifericos", topicos: ["i2s"] },
{ campo: "pwm", label: "PWM", secao: "perifericos", topicos: ["pwm"] },
{ campo: "usb", label: "USB", secao: "perifericos", topicos: ["usb"] },
{ campo: "lcd", label: "LCD", secao: "interfaces", topicos: ["lcd"] },
{ campo: "camera", label: "摄像头", secao: "interfaces", topicos: ["camera"] },
{ campo: "mipi_csi", label: "MIPI CSI", secao: "interfaces", topicos: ["camera"] },
{ campo: "mipi_dsi", label: "MIPI DSI", secao: "interfaces", topicos: ["lcd"] },
{ campo: "sdio", label: "SDIO", secao: "interfaces", topicos: ["sd"] },
{ campo: "sensor_temperatura", label: "温度传感器", secao: "interfaces", topicos: ["temperatura"] },
{ campo: "seguranca.aes", label: "AES", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.sha", label: "SHA", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.rsa", label: "RSA", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.ecc", label: "ECC", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.ecdsa", label: "ECDSA", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.hmac", label: "HMAC", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.assinatura_digital", label: "数字签名", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.rng", label: "随机数发生器", secao: "seguranca", topicos: ["seguranca"] },
{ campo: "seguranca.criptografia_flash", label: "闪存加密", secao: "seguranca", topicos: ["seguranca", "flash"] },
{ campo: "seguranca.secure_boot", label: "安全启动", secao: "seguranca", topicos: ["seguranca", "strapping"] },
{ campo: "temperatura_operacao", label: "工作温度", secao: "temperatura", topicos: ["temperatura"] },
{ campo: "temperatura_armazenamento", label: "存储温度", secao: "temperatura", topicos: ["temperatura"] },
{ campo: "esp_idf_minimo", label: "最低 ESP-IDF 版本", secao: "ferramentas", topicos: ["ferramentas"] },
{ campo: "arduino_core", label: "Arduino 核心", secao: "ferramentas", topicos: ["ferramentas"] },
{ campo: "depuracao", label: "调试", secao: "ferramentas", topicos: ["jtag", "ferramentas"] },
{ campo: "encapsulamento", label: "封装", secao: "ferramentas", topicos: ["encapsulamento"] },
{ campo: "modulos", label: "模组", secao: "ferramentas", topicos: ["encapsulamento"] },
];
const ROTULOS_ENERGIA = {
active: "工作",
modem_sleep: "Modem Sleep",
light_sleep: "Light Sleep",
deep_sleep: "Deep Sleep",
hibernation: "Hibernation",
};
const pegar = (obj, caminho) => caminho.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
/**
* Monta, para cada série, a lista plana de especificações (com os assuntos a
* que respondem) e os pinos do diagrama. É o que o navegador baixa.
*
* @param {Record<string, object>} series conteúdo de public/series.json
* @param {Record<string, object[]>} conexoes pinos por série (public/conexoes)
*/
export function montarConhecimento(series, conexoes) {
const saida = {};
for (const [chave, s] of Object.entries(series)) {
const specs = [];
for (const c of SPEC_CAMPOS) {
const valor = pegar(s, c.campo);
if (valor == null || valor === "") continue;
specs.push({ label: c.label, valor: String(valor), secao: c.secao, topicos: c.topicos });
}
for (const [modo, valor] of Object.entries(s.consumo_energia ?? {})) {
const base = modo.replace(/_cpu.*|_wifi.*/, "");
const label = ROTULOS_ENERGIA[modo] ?? ROTULOS_ENERGIA[base] ?? modo.replace(/_/g, " ");
specs.push({ label: `功耗:${label}`, valor: String(valor), secao: "energia", topicos: ["consumo"] });
}
saida[chave] = {
nome: s.nome_completo ?? chave,
specs,
pinos: (conexoes[chave] ?? []).map((p) => ({
n: String(p.numero),
nome: p.nome,
f: p.funcoes ?? [],
c: p.categorias ?? [],
a: p.avisos ?? [],
})),
};
}
return saida;
}
+79 -24
View File
@@ -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 }));
+146
View File
@@ -0,0 +1,146 @@
/**
* Textos e rotas das respostas diretas da busca, por idioma.
*
* Módulo puro (sem alias de import): o servidor monta o índice com ele e o
* motor (`searchEngine.js`) só preenche os {placeholders}. Idioma sem entrada
* cai no português.
*/
const base = (loc, series, comparacao) => ({ series: `/${loc}/${series}`, comparacao: `/${loc}/${comparacao}` });
export const BUSCA_I18N = {
pt: {
rotas: base("pt", "series", "comparacao"),
t: {
specTitulo: "{topico} no {serie}",
specDesc: "Dados da série",
pinosTitulo: "Pinos do {serie} {rotulo}",
pinosMistos: "{forte} ocupados + {fraco} que dependem do modo de uso (ex.: SPI de 8 linhas) · abre o diagrama filtrado",
pinoUm: "{n} pino · abre o diagrama já filtrado",
pinosVarios: "{n} pinos · abre o diagrama já filtrado",
pinosFraco: "{n} pinos que só são afetados em certos modos de uso · abre o diagrama filtrado",
condicionais: "+{n} condicionais",
motivoTopico: "{topico}: pinos {rotulo}",
diagTitulo: "Diagrama de pinos do {serie}",
diagDesc: "{n} pinos com funções, restrições e avisos",
especTitulo: "Especificações do {serie}",
especDesc: "Processador, memória, conectividade, periféricos e mais",
emCada: "{topico} em cada série",
compDesc: "Comparativo rápido · adicione o chip na busca para detalhar",
pinoNo: "{pino} no {serie}",
motivoPino: "Pino {pino}",
},
},
en: {
rotas: base("en", "series", "compare"),
t: {
specTitulo: "{topico} on the {serie}",
specDesc: "Series data",
pinosTitulo: "{serie}: {rotulo}",
pinosMistos: "{forte} occupied + {fraco} that depend on the usage mode (e.g. 8-line SPI) · opens the filtered diagram",
pinoUm: "{n} pin · opens the already filtered diagram",
pinosVarios: "{n} pins · opens the already filtered diagram",
pinosFraco: "{n} pins only affected in certain usage modes · opens the filtered diagram",
condicionais: "+{n} conditional",
motivoTopico: "{topico}: {rotulo}",
diagTitulo: "{serie} pinout diagram",
diagDesc: "{n} pins with functions, restrictions and warnings",
especTitulo: "{serie} specifications",
especDesc: "Processor, memory, connectivity, peripherals and more",
emCada: "{topico} across all series",
compDesc: "Quick comparison · add the chip to your search for details",
pinoNo: "{pino} on the {serie}",
motivoPino: "Pin {pino}",
},
},
es: {
rotas: base("es", "series", "comparacion"),
t: {
specTitulo: "{topico} en el {serie}",
specDesc: "Datos de la serie",
pinosTitulo: "Pines del {serie} {rotulo}",
pinosMistos: "{forte} ocupados + {fraco} que dependen del modo de uso (p. ej., SPI de 8 líneas) · abre el diagrama filtrado",
pinoUm: "{n} pin · abre el diagrama ya filtrado",
pinosVarios: "{n} pines · abre el diagrama ya filtrado",
pinosFraco: "{n} pines que solo se ven afectados en ciertos modos de uso · abre el diagrama filtrado",
condicionais: "+{n} condicionales",
motivoTopico: "{topico}: pines {rotulo}",
diagTitulo: "Diagrama de pines del {serie}",
diagDesc: "{n} pines con funciones, restricciones y avisos",
especTitulo: "Especificaciones del {serie}",
especDesc: "Procesador, memoria, conectividad, periféricos y más",
emCada: "{topico} en cada serie",
compDesc: "Comparativa rápida · añade el chip a la búsqueda para ver el detalle",
pinoNo: "{pino} en el {serie}",
motivoPino: "Pin {pino}",
},
},
fr: {
rotas: base("fr", "series", "comparaison"),
t: {
specTitulo: "{topico} sur le {serie}",
specDesc: "Données de la série",
pinosTitulo: "Broches du {serie} {rotulo}",
pinosMistos: "{forte} occupées + {fraco} qui dépendent du mode d'utilisation (ex. : SPI 8 lignes) · ouvre le schéma filtré",
pinoUm: "{n} broche · ouvre le schéma déjà filtré",
pinosVarios: "{n} broches · ouvre le schéma déjà filtré",
pinosFraco: "{n} broches concernées seulement dans certains modes d'utilisation · ouvre le schéma filtré",
condicionais: "+{n} conditionnelles",
motivoTopico: "{topico} : broches {rotulo}",
diagTitulo: "Schéma des broches du {serie}",
diagDesc: "{n} broches avec fonctions, restrictions et avertissements",
especTitulo: "Caractéristiques du {serie}",
especDesc: "Processeur, mémoire, connectivité, périphériques et plus",
emCada: "{topico} dans chaque série",
compDesc: "Comparatif rapide · ajoutez la puce à la recherche pour détailler",
pinoNo: "{pino} sur le {serie}",
motivoPino: "Broche {pino}",
},
},
de: {
rotas: base("de", "serien", "vergleich"),
t: {
specTitulo: "{topico} beim {serie}",
specDesc: "Seriendaten",
pinosTitulo: "{serie}: {rotulo}",
pinosMistos: "{forte} belegt + {fraco} abhängig vom Nutzungsmodus (z. B. 8-Bit-SPI) · öffnet das gefilterte Diagramm",
pinoUm: "{n} Pin · öffnet das bereits gefilterte Diagramm",
pinosVarios: "{n} Pins · öffnet das bereits gefilterte Diagramm",
pinosFraco: "{n} Pins, die nur in bestimmten Nutzungsmodi betroffen sind · öffnet das gefilterte Diagramm",
condicionais: "+{n} bedingte",
motivoTopico: "{topico}: {rotulo}",
diagTitulo: "Pin-Diagramm des {serie}",
diagDesc: "{n} Pins mit Funktionen, Einschränkungen und Hinweisen",
especTitulo: "Spezifikationen des {serie}",
especDesc: "Prozessor, Speicher, Konnektivität, Peripherie und mehr",
emCada: "{topico} in jeder Serie",
compDesc: "Schnellvergleich · fügen Sie den Chip zur Suche hinzu, um Details zu sehen",
pinoNo: "{pino} beim {serie}",
motivoPino: "Pin {pino}",
},
},
"zh-CN": {
rotas: base("zh-CN", "series", "compare"),
t: {
specTitulo: "{serie}:{topico}",
specDesc: "系列数据",
pinosTitulo: "{serie} 的引脚:{rotulo}",
pinosMistos: "{forte} 个被占用 + {fraco} 个取决于使用模式(例如八线 SPI)· 打开已筛选的引脚图",
pinoUm: "{n} 个引脚 · 打开已筛选的引脚图",
pinosVarios: "{n} 个引脚 · 打开已筛选的引脚图",
pinosFraco: "{n} 个引脚仅在某些使用模式下受影响 · 打开已筛选的引脚图",
condicionais: "另有 {n} 个有条件",
motivoTopico: "{topico}:{rotulo}",
diagTitulo: "{serie} 引脚图",
diagDesc: "{n} 个引脚,含功能、限制与警告",
especTitulo: "{serie} 规格",
especDesc: "处理器、内存、无线连接、外设等",
emCada: "{topico}:各系列对比",
compDesc: "快速对比 · 在搜索中加上芯片可查看详情",
pinoNo: "{serie} 的 {pino}",
motivoPino: "引脚 {pino}",
},
},
};
export const buscaI18n = (locale) => BUSCA_I18N[locale] ?? BUSCA_I18N.pt;
+47 -6
View File
@@ -33,6 +33,20 @@ import { errosCatalogo as errosDe } from "@/lib/data/de/diagnostico";
import { COMPONENTES_REGISTRY as componentesDe } from "@/lib/data/de/componentes";
import { montarConhecimento as montarConhecimentoDe } from "@/lib/data/de/topicos";
import seriesDataZh from "@/data/zh-CN/series.json";
import frameworksDataZh from "@/data/zh-CN/frameworks.json";
import { categoriasGlossario as glossarioZh } from "@/lib/data/zh-CN/glossario";
import { errosCatalogo as errosZh } from "@/lib/data/zh-CN/diagnostico";
import { COMPONENTES_REGISTRY as componentesZh } from "@/lib/data/zh-CN/componentes";
import { montarConhecimento as montarConhecimentoZh } from "@/lib/data/zh-CN/topicos";
// Nomes, rótulos e sinônimos dos assuntos no idioma da página (o motor só traz os em português).
import { TOPICOS as TOPICOS_EN } from "@/lib/data/en/topicos";
import { TOPICOS as TOPICOS_ES } from "@/lib/data/es/topicos";
import { TOPICOS as TOPICOS_FR } from "@/lib/data/fr/topicos";
import { TOPICOS as TOPICOS_DE } from "@/lib/data/de/topicos";
import { TOPICOS as TOPICOS_ZH } from "@/lib/data/zh-CN/topicos";
import { readFileSync, existsSync } from "node:fs";
import path from "node:path";
@@ -97,6 +111,19 @@ const PAGINAS_FR = [
{ titulo: "À Propos du Projet", descricao: "Histoire, auteur et open source", href: "/fr/a-propos", keywords: "auteur contact open source" },
];
const PAGINAS_ZH = [
{ titulo: "首页", descricao: "ESPDocs 首页", href: "/zh-CN", keywords: "首页 主页 home" },
{ titulo: "全部系列", descricao: "每个 ESP32 系列的引脚、规格与限制", href: "/zh-CN/series", keywords: "芯片 引脚 规格 pinout" },
{ titulo: "系列对比", descricao: "最多并排对比 4 个型号", href: "/zh-CN/compare", keywords: "对比 比较 差异" },
{ titulo: "开发板目录", descricao: "开发板、引脚与购买渠道", href: "/zh-CN/catalog", keywords: "开发板 devkit 购买" },
{ titulo: "智能选型", descricao: "为你的项目找到合适的芯片", href: "/zh-CN/selector", keywords: "选型 推荐 测验" },
{ titulo: "故障诊断", descricao: "排查 Brownout、启动循环、看门狗和崩溃", href: "/zh-CN/diagnostics", keywords: "故障 错误 崩溃 问题 bug" },
{ titulo: "框架与 SDK", descricao: "ESP-IDF、Arduino、Rust、RainMaker 等", href: "/zh-CN/frameworks", keywords: "sdk 开发框架" },
{ titulo: "Component Registry", descricao: "驱动目录与 idf_component.yml 生成器", href: "/zh-CN/components", keywords: "库 驱动 组件 yml" },
{ titulo: "技术术语表", descricao: "关键的硬件与软件概念", href: "/zh-CN/glossary", keywords: "术语 词典 定义 含义" },
{ titulo: "关于本项目", descricao: "历史、作者与开源", href: "/zh-CN/about", keywords: "作者 联系 开源" },
];
const PAGINAS_DE = [
{ titulo: "Startseite", descricao: "ESPDocs Startseite", href: "/de", keywords: "startseite home" },
{ titulo: "Alle Serien", descricao: "Pinbelegungen, Spezifikationen und Einschränkungen für jede ESP32-Serie", href: "/de/serien", keywords: "chips pinbelegung spezifikationen pinout" },
@@ -117,6 +144,7 @@ export function construirIndiceBusca(locale = "pt") {
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const isZh = locale === "zh-CN";
let paginas = PAGINAS_PT;
let seriesData = seriesDataPt;
@@ -158,13 +186,21 @@ export function construirIndiceBusca(locale = "pt") {
errosCatalogo = errosDe;
componentes = componentesDe;
montarConhecimento = montarConhecimentoDe;
} else if (isZh) {
paginas = PAGINAS_ZH;
seriesData = seriesDataZh;
frameworksData = frameworksDataZh;
categoriasGlossario = glossarioZh;
errosCatalogo = errosZh;
componentes = componentesZh;
montarConhecimento = montarConhecimentoZh;
}
const slugSeries = isDe ? "/de/serien" : isFr ? "/fr/series" : isEn ? "/en/series" : isEs ? "/es/series" : "/series";
const slugFrameworks = isDe ? "/de/frameworks" : isFr ? "/fr/frameworks" : isEn ? "/en/frameworks" : isEs ? "/es/frameworks" : "/frameworks";
const slugGlossario = isDe ? "/de/glossar" : isFr ? "/fr/glossaire" : isEn ? "/en/glossary" : isEs ? "/es/glosario" : "/glossario";
const slugDiagnostico = isDe ? "/de/diagnose" : isFr ? "/fr/diagnostic" : isEn ? "/en/diagnostics" : isEs ? "/es/diagnostico" : "/diagnostico";
const slugComponentes = isDe ? "/de/komponenten" : isFr ? "/fr/composants" : isEn ? "/en/components" : isEs ? "/es/componentes" : "/componentes";
const slugSeries = isZh ? "/zh-CN/series" : isDe ? "/de/serien" : isFr ? "/fr/series" : isEn ? "/en/series" : isEs ? "/es/series" : "/series";
const slugFrameworks = isZh ? "/zh-CN/frameworks" : isDe ? "/de/frameworks" : isFr ? "/fr/frameworks" : isEn ? "/en/frameworks" : isEs ? "/es/frameworks" : "/frameworks";
const slugGlossario = isZh ? "/zh-CN/glossary" : isDe ? "/de/glossar" : isFr ? "/fr/glossaire" : isEn ? "/en/glossary" : isEs ? "/es/glosario" : "/glossario";
const slugDiagnostico = isZh ? "/zh-CN/diagnostics" : isDe ? "/de/diagnose" : isFr ? "/fr/diagnostic" : isEn ? "/en/diagnostics" : isEs ? "/es/diagnostico" : "/diagnostico";
const slugComponentes = isZh ? "/zh-CN/components" : isDe ? "/de/komponenten" : isFr ? "/fr/composants" : isEn ? "/en/components" : isEs ? "/es/componentes" : "/componentes";
const itens = [];
@@ -233,7 +269,7 @@ export function construirIndiceBusca(locale = "pt") {
? path.join(process.cwd(), "data", "es", "conexoes")
: isFr
? path.join(process.cwd(), "data", "fr", "conexoes")
: isDe
: isZh ? path.join(process.cwd(), "data", "zh-CN", "conexoes") : isDe
? path.join(process.cwd(), "data", "de", "conexoes")
: path.join(process.cwd(), "public", "conexoes");
const arquivo = path.join(pastaConexoes, `${chave}.json`);
@@ -245,7 +281,12 @@ export function construirIndiceBusca(locale = "pt") {
}
}
const topicosLoc = { en: TOPICOS_EN, es: TOPICOS_ES, fr: TOPICOS_FR, de: TOPICOS_DE, "zh-CN": TOPICOS_ZH }[locale];
return {
locale,
topicos: topicosLoc
? Object.fromEntries(topicosLoc.map((tp) => [tp.id, { nome: tp.nome, pinoRotulo: tp.pinoRotulo, termos: tp.termos }]))
: null,
itens: itens.map((item, i) => ({ id: i, ...item })),
series: montarConhecimento(seriesData, conexoes),
};
+37 -31
View File
@@ -97,9 +97,10 @@ export const jsonLd = {
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const orgAnchor = isEn ? "organization" : isEs ? "organizacion" : (isFr || isDe) ? "organisation" : "organizacao";
const isZh = locale === "zh-CN";
const orgAnchor = isZh ? "organization" : isEn ? "organization" : isEs ? "organizacion" : (isFr || isDe) ? "organisation" : "organizacao";
const authorAnchor = isEn ? "author" : isFr ? "auteur" : "autor";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isDe ? "de" : "pt-BR";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isZh ? "zh-CN" : isDe ? "de" : "pt-BR";
return {
"@context": "https://schema.org",
"@graph": [
@@ -115,7 +116,7 @@ export const jsonLd = {
? "Documentación del ecosistema ESP32: especificaciones, patillaje, comparación y selección de series."
: isFr
? "Documentation de l'écosystème ESP32 : spécifications, brochages, comparaison et sélection de séries."
: isDe
: isZh ? "ESP32 生态文档(简体中文):规格、引脚、对比与系列选型。" : isDe
? "Dokumentation des ESP32-Ökosystems: Spezifikationen, Pinbelegung, Vergleich und Serienauswahl."
: "Documentação do ecossistema ESP32 em português brasileiro: especificações, pinagem, comparação e seleção de séries.",
publisher: { "@id": `${SITE.url}/#${orgAnchor}` },
@@ -166,33 +167,34 @@ export const jsonLd = {
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const isZh = locale === "zh-CN";
const prop = (nome, valor) =>
valor ? { "@type": "PropertyValue", name: nome, value: String(valor) } : null;
const propriedades = [
prop(isEn ? "Manufacturer" : isEs ? "Fabricante" : isFr ? "Fabricant" : isDe ? "Hersteller" : "Fabricante", "Espressif Systems"),
prop(isEn ? "Architecture" : isEs ? "Arquitectura" : isFr ? "Architecture" : isDe ? "Architektur" : "Arquitetura"),
prop(isEn ? "Cores" : isEs ? "Núcleos" : isFr ? "Cœurs" : isDe ? "Kerne" : "Núcleos", serie.nucleos),
prop(isEn ? "Max frequency" : isEs ? "Frecuencia máxima" : isFr ? "Fréquence maximale" : isDe ? "Maximale Frequenz" : "Frequência máxima", serie.frequencia),
prop(isEn ? "Manufacturer" : isEs ? "Fabricante" : isFr ? "Fabricant" : isZh ? "制造商" : isDe ? "Hersteller" : "Fabricante", "Espressif Systems"),
prop(isEn ? "Architecture" : isEs ? "Arquitectura" : isFr ? "Architecture" : isZh ? "架构" : isDe ? "Architektur" : "Arquitetura"),
prop(isEn ? "Cores" : isEs ? "Núcleos" : isFr ? "Cœurs" : isZh ? "内核数" : isDe ? "Kerne" : "Núcleos", serie.nucleos),
prop(isEn ? "Max frequency" : isEs ? "Frecuencia máxima" : isFr ? "Fréquence maximale" : isZh ? "最高频率" : isDe ? "Maximale Frequenz" : "Frequência máxima", serie.frequencia),
prop("SRAM", serie.memoria_sram),
prop("RTC SRAM", serie.memoria_sram_rtc),
prop(isEn ? "Supported Flash" : isEs ? "Flash soportada" : isFr ? "Flash prise en charge" : isDe ? "Unterstützter Flash" : "Flash suportada", serie.flash_suportada),
prop(isEn ? "Supported PSRAM" : isEs ? "PSRAM soportada" : isFr ? "PSRAM prise en charge" : isDe ? "Unterstütztes PSRAM" : "PSRAM suportada", serie.psram_suportada),
prop(isEn ? "Supported Flash" : isEs ? "Flash soportada" : isFr ? "Flash prise en charge" : isZh ? "支持的闪存" : isDe ? "Unterstützter Flash" : "Flash suportada", serie.flash_suportada),
prop(isEn ? "Supported PSRAM" : isEs ? "PSRAM soportada" : isFr ? "PSRAM prise en charge" : isZh ? "支持的 PSRAM" : isDe ? "Unterstütztes PSRAM" : "PSRAM suportada", serie.psram_suportada),
prop("GPIO", serie.gpio),
prop("Wi-Fi", serie.wifi),
prop("Bluetooth", serie.bluetooth),
prop("Zigbee / Thread / 802.15.4", serie.zigbee_thread),
prop(isEn ? "Package" : isEs ? "Encapsulado" : isFr ? "Boîtier" : isDe ? "Gehäuse" : "Encapsulamento", serie.encapsulamento),
prop(isEn ? "Minimum ESP-IDF" : isEs ? "ESP-IDF mínimo" : isFr ? "ESP-IDF minimum" : isDe ? "Mindestens ESP-IDF" : "ESP-IDF mínimo", serie.esp_idf_minimo),
prop(isEn ? "Package" : isEs ? "Encapsulado" : isFr ? "Boîtier" : isZh ? "封装" : isDe ? "Gehäuse" : "Encapsulamento", serie.encapsulamento),
prop(isEn ? "Minimum ESP-IDF" : isEs ? "ESP-IDF mínimo" : isFr ? "ESP-IDF minimum" : isZh ? "最低 ESP-IDF 版本" : isDe ? "Mindestens ESP-IDF" : "ESP-IDF mínimo", serie.esp_idf_minimo),
].filter(Boolean);
const imagem = serie.placa
? `${SITE.url}${serie.placa}`
: `${SITE.url}/opengraph-image.png`;
const basePath = isDe ? `/de/serien/${chave}` : isFr ? `/fr/series/${chave}` : isEn ? `/en/series/${chave}` : isEs ? `/es/series/${chave}` : `/series/${chave}`;
const anchor = isEn ? "article" : isEs ? "articulo" : isFr ? "article" : isDe ? "artikel" : "artigo";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isDe ? "de" : "pt-BR";
const basePath = isZh ? `/zh-CN/series/${chave}` : isDe ? `/de/serien/${chave}` : isFr ? `/fr/series/${chave}` : isEn ? `/en/series/${chave}` : isEs ? `/es/series/${chave}` : `/series/${chave}`;
const anchor = isEn ? "article" : isEs ? "articulo" : isFr ? "article" : isZh ? "article" : isDe ? "artikel" : "artigo";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isZh ? "zh-CN" : isDe ? "de" : "pt-BR";
return {
"@context": "https://schema.org",
@@ -204,7 +206,7 @@ export const jsonLd = {
? `${serie.nome_completo || chave}: especificaciones y patillaje`
: isFr
? `${serie.nome_completo || chave} : spécifications et brochage`
: isDe
: isZh ? `${serie.nome_completo || chave}:规格与引脚图` : isDe
? `${serie.nome_completo || chave}: Spezifikationen und Pinbelegung`
: `${serie.nome_completo || chave}: especificações e pinagem`,
name: isEn
@@ -213,7 +215,7 @@ export const jsonLd = {
? `${serie.nome_completo || chave} - Documentación técnica y especificaciones`
: isFr
? `${serie.nome_completo || chave} - Documentation technique et spécifications`
: isDe
: isZh ? `${serie.nome_completo || chave} - 技术文档与规格` : isDe
? `${serie.nome_completo || chave} - Technische Dokumentation und Spezifikationen`
: `${serie.nome_completo || chave} - Documentação Técnica e Especificações`,
description: serie.descricao,
@@ -254,9 +256,10 @@ export const jsonLd = {
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const basePath = isDe ? `/de/frameworks/${encodeURIComponent(chave)}` : isFr ? `/fr/frameworks/${encodeURIComponent(chave)}` : isEn ? `/en/frameworks/${encodeURIComponent(chave)}` : isEs ? `/es/frameworks/${encodeURIComponent(chave)}` : `/frameworks/${encodeURIComponent(chave)}`;
const anchor = isEn ? "article" : isEs ? "articulo" : isFr ? "article" : isDe ? "artikel" : "artigo";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isDe ? "de" : "pt-BR";
const isZh = locale === "zh-CN";
const basePath = isZh ? `/zh-CN/frameworks/${encodeURIComponent(chave)}` : isDe ? `/de/frameworks/${encodeURIComponent(chave)}` : isFr ? `/fr/frameworks/${encodeURIComponent(chave)}` : isEn ? `/en/frameworks/${encodeURIComponent(chave)}` : isEs ? `/es/frameworks/${encodeURIComponent(chave)}` : `/frameworks/${encodeURIComponent(chave)}`;
const anchor = isEn ? "article" : isEs ? "articulo" : isFr ? "article" : isZh ? "article" : isDe ? "artikel" : "artigo";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isZh ? "zh-CN" : isDe ? "de" : "pt-BR";
return {
"@context": "https://schema.org",
"@type": "TechArticle",
@@ -267,14 +270,14 @@ export const jsonLd = {
? `${framework.nome} para ESP32: Guía y documentación`
: isFr
? `${framework.nome} pour ESP32 : Guide et documentation`
: isDe
: isZh ? `用于 ESP32 的 ${framework.nome}:指南与文档` : isDe
? `${framework.nome} für ESP32: Leitfaden und Dokumentation`
: `${framework.nome} para ESP32: Guia e Documentação`,
name: isEn
? `${framework.nome_completo || framework.nome} for ESP32`
: isFr
? `${framework.nome_completo || framework.nome} pour ESP32`
: isDe
: isZh ? `用于 ESP32 的 ${framework.nome_completo || framework.nome}` : isDe
? `${framework.nome_completo || framework.nome} für ESP32`
: `${framework.nome_completo || framework.nome} para ESP32`,
description: framework.descricao,
@@ -317,14 +320,15 @@ export const jsonLd = {
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const basePath = isDe ? "/de/glossar" : isFr ? "/fr/glossaire" : isEn ? "/en/glossary" : isEs ? "/es/glosario" : "/glossario";
const anchor = isDe ? "glossar" : isFr ? "glossaire" : isEn ? "glossary" : isEs ? "glosario" : "glossario";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isDe ? "de" : "pt-BR";
const isZh = locale === "zh-CN";
const basePath = isZh ? "/zh-CN/glossary" : isDe ? "/de/glossar" : isFr ? "/fr/glossaire" : isEn ? "/en/glossary" : isEs ? "/es/glosario" : "/glossario";
const anchor = isZh ? "glossary" : isDe ? "glossar" : isFr ? "glossaire" : isEn ? "glossary" : isEs ? "glosario" : "glossario";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isZh ? "zh-CN" : isDe ? "de" : "pt-BR";
return {
"@context": "https://schema.org",
"@type": "DefinedTermSet",
"@id": `${SITE.url}${basePath}#${anchor}`,
name: isEn ? "ESP32 Technical Glossary" : isEs ? "Glosario técnico de ESP32" : isFr ? "Glossaire technique de l'ESP32" : isDe ? "Technisches Glossar für ESP32" : "Glossário Técnico do ESP32",
name: isEn ? "ESP32 Technical Glossary" : isEs ? "Glosario técnico de ESP32" : isFr ? "Glossaire technique de l'ESP32" : isZh ? "ESP32 技术术语表" : isDe ? "Technisches Glossar für ESP32" : "Glossário Técnico do ESP32",
inLanguage: inLang,
hasDefinedTerm: categorias.flatMap((cat) =>
cat.termos.map((t) => ({
@@ -356,7 +360,8 @@ export const jsonLd = {
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isDe ? "de" : "pt-BR";
const isZh = locale === "zh-CN";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isZh ? "zh-CN" : isDe ? "de" : "pt-BR";
return {
"@context": "https://schema.org",
"@type": "WebApplication",
@@ -452,21 +457,22 @@ export const jsonLd = {
const isEs = locale === "es";
const isFr = locale === "fr";
const isDe = locale === "de";
const basePath = isDe ? "/de/ueber-uns" : isFr ? "/fr/a-propos" : isEn ? "/en/about" : isEs ? "/es/acerca" : "/sobre";
const anchor = isDe ? "ueber-uns" : isFr ? "a-propos" : isEn ? "about" : isEs ? "acerca" : "sobre";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isDe ? "de" : "pt-BR";
const isZh = locale === "zh-CN";
const basePath = isZh ? "/zh-CN/about" : isDe ? "/de/ueber-uns" : isFr ? "/fr/a-propos" : isEn ? "/en/about" : isEs ? "/es/acerca" : "/sobre";
const anchor = isZh ? "about" : isDe ? "ueber-uns" : isFr ? "a-propos" : isEn ? "about" : isEs ? "acerca" : "sobre";
const inLang = isEn ? "en" : isEs ? "es-419" : isFr ? "fr" : isZh ? "zh-CN" : isDe ? "de" : "pt-BR";
return {
"@context": "https://schema.org",
"@type": "AboutPage",
"@id": `${SITE.url}${basePath}#${anchor}`,
name: isEn ? "About ESPDocs" : isEs ? "Acerca de ESPDocs" : isFr ? "À propos d'ESPDocs" : isDe ? "Über ESPDocs" : "Sobre o ESPDocs",
name: isEn ? "About ESPDocs" : isEs ? "Acerca de ESPDocs" : isFr ? "À propos d'ESPDocs" : isZh ? "关于 ESPDocs" : isDe ? "Über ESPDocs" : "Sobre o ESPDocs",
description: isEn
? "ESPDocs is an independent platform organizing technical documentation of the ESP32 ecosystem."
: isEs
? "ESPDocs es una plataforma independiente que reúne la documentación técnica del ecosistema ESP32."
: isFr
? "ESPDocs est une plateforme indépendante qui rassemble la documentation technique de l'écosystème ESP32."
: isDe
: isZh ? "ESPDocs 是一个独立平台,汇集 ESP32 生态的文档,使用简体中文。" : isDe
? "ESPDocs ist eine unabhängige Plattform, die die technische Dokumentation des ESP32-Ökosystems zusammenführt."
: "O ESPDocs é uma plataforma independente que reúne a documentação do ecossistema ESP32 em português brasileiro.",
url: `${SITE.url}${basePath}`,