feat(i18n): French and German localization (phase 5)
- Add full message catalogs and content for French (fr) and German (de) - Implement ASCII canonical slugs for French and German routes - Add language-specific search endpoints (/api/busca/fr, /api/busca/de) - Configure strict 404 responses for /fr/livro and /de/livro - Localize SEO, OpenGraph (fr_FR, de_DE), Twitter and JSON-LD schemas - Add localized code example comments across 14 examples for fr and de - Enable 'fr' and 'de' in ENABLED_LOCALES with root negotiation and redirect - Extend test suite in check-i18n, smoke-imagem and verifica-vazamentos Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
1 parent
7b673aeb4b
commit
7fce46b9e1
104 files changed
+33369
-686
No files matched your search
@@ -41,10 +41,50 @@ const HEADER_MAP_ES = {
|
||||
"LEDs": "LEDs",
|
||||
};
|
||||
|
||||
const HEADER_MAP_FR = {
|
||||
"Nome": "Nom",
|
||||
"Descrição": "Description",
|
||||
"Série": "Série",
|
||||
"Loja Oficial": "Boutique officielle",
|
||||
"Fabricante": "Fabricant",
|
||||
"Processador": "Processeur",
|
||||
"Memória Flash": "Mémoire flash",
|
||||
"PSRAM": "PSRAM",
|
||||
"Conector USB": "Connecteur USB",
|
||||
"Interface USB": "Interface USB",
|
||||
"Antena": "Antenne",
|
||||
"Bateria": "Batterie",
|
||||
"Display": "Écran",
|
||||
"Sensores": "Capteurs",
|
||||
"Botões": "Boutons",
|
||||
"LEDs": "LEDs",
|
||||
};
|
||||
|
||||
const HEADER_MAP_DE = {
|
||||
"Nome": "Name",
|
||||
"Descrição": "Beschreibung",
|
||||
"Série": "Serie",
|
||||
"Loja Oficial": "Offizieller Shop",
|
||||
"Fabricante": "Hersteller",
|
||||
"Processador": "Prozessor",
|
||||
"Memória Flash": "Flash-Speicher",
|
||||
"PSRAM": "PSRAM",
|
||||
"Conector USB": "USB-Anschluss",
|
||||
"Interface USB": "USB-Schnittstelle",
|
||||
"Antena": "Antenne",
|
||||
"Bateria": "Batterie",
|
||||
"Display": "Display",
|
||||
"Sensores": "Sensoren",
|
||||
"Botões": "Taster",
|
||||
"LEDs": "LEDs",
|
||||
};
|
||||
|
||||
export default function BoardCard({ board }) {
|
||||
const pathname = usePathname() || "";
|
||||
const isEn = pathname === "/en" || pathname.startsWith("/en/");
|
||||
const isEs = pathname === "/es" || pathname.startsWith("/es/");
|
||||
const isFr = pathname === "/fr" || pathname.startsWith("/fr/");
|
||||
const isDe = pathname === "/de" || pathname.startsWith("/de/");
|
||||
|
||||
const mainFields = ["Nome", "Link", "Imagem", "Loja Oficial"];
|
||||
|
||||
@@ -67,21 +107,31 @@ export default function BoardCard({ board }) {
|
||||
{lojaOficial && (
|
||||
<span className="chip chip-accent mb-3 w-fit">
|
||||
<Award className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{isEn ? "Official store" : isEs ? "Tienda oficial" : "Loja oficial"}
|
||||
{isEn ? "Official store" : isEs ? "Tienda oficial" : isFr ? "Boutique officielle" : isDe ? "Offizieller Shop" : "Loja oficial"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<h3 className="line-clamp-2 font-display text-lg font-semibold leading-snug tracking-[-0.02em] text-ink">
|
||||
{board.Nome || (isEn ? "Generic board" : isEs ? "Placa genérica" : "Placa genérica")}
|
||||
{board.Nome || (isEn ? "Generic board" : isEs ? "Placa genérica" : isFr ? "Carte générique" : isDe ? "Generisches Board" : "Placa genérica")}
|
||||
</h3>
|
||||
|
||||
{recursos.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="text-xs font-medium text-muted">{isEn ? "On-board features" : isEs ? "Recursos integrados" : "Recursos embarcados"}</p>
|
||||
<p className="text-xs font-medium text-muted">
|
||||
{isEn ? "On-board features" : isEs ? "Recursos integrados" : isFr ? "Fonctionnalités intégrées" : isDe ? "Integrierte Funktionen" : "Recursos embarcados"}
|
||||
</p>
|
||||
<ul className="mt-2 flex flex-wrap gap-1.5">
|
||||
{recursos.map((campo) => {
|
||||
const opcional = valor(campo) === "opcional";
|
||||
const label = isEn ? (HEADER_MAP_EN[campo] || campo) : isEs ? (HEADER_MAP_ES[campo] || campo) : campo;
|
||||
const label = isEn
|
||||
? (HEADER_MAP_EN[campo] || campo)
|
||||
: isEs
|
||||
? (HEADER_MAP_ES[campo] || campo)
|
||||
: isFr
|
||||
? (HEADER_MAP_FR[campo] || campo)
|
||||
: isDe
|
||||
? (HEADER_MAP_DE[campo] || campo)
|
||||
: campo;
|
||||
return (
|
||||
<li key={campo} className={`chip ${opcional ? "text-gold" : "text-ink-soft"}`}>
|
||||
{opcional ? (
|
||||
@@ -90,7 +140,7 @@ export default function BoardCard({ board }) {
|
||||
<Check className="h-3 w-3 text-ok" aria-hidden="true" />
|
||||
)}
|
||||
{label}
|
||||
{opcional && <span className="sr-only">{isEn ? " (optional)" : " (opcional)"}</span>}
|
||||
{opcional && <span className="sr-only">{isEn ? " (optional)" : isEs ? " (opcional)" : isFr ? " (optionnel)" : isDe ? " (optional)" : " (opcional)"}</span>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -100,9 +150,11 @@ export default function BoardCard({ board }) {
|
||||
|
||||
{board.Link && (
|
||||
<a href={board.Link} target="_blank" rel="noopener noreferrer" className="btn btn-primary mt-6 w-full">
|
||||
{isEn ? "View on AliExpress" : isEs ? "Ver en AliExpress" : "Ver no AliExpress"}
|
||||
{isEn ? "View on AliExpress" : isEs ? "Ver en AliExpress" : isFr ? "Voir sur AliExpress" : isDe ? "Auf AliExpress ansehen" : "Ver no AliExpress"}
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only">{isEn ? " (opens in new tab)" : isEs ? " (abre en una pestaña nueva)" : " (abre em nova aba)"}</span>
|
||||
<span className="sr-only">
|
||||
{isEn ? " (opens in new tab)" : isEs ? " (abre en una pestaña nueva)" : isFr ? " (ouvre un nouvel onglet)" : isDe ? " (öffnet in neuem Tab)" : " (abre em nova aba)"}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+110
-20
@@ -8,7 +8,9 @@ export default function CodeExample({ framework }) {
|
||||
const pathname = usePathname() || "";
|
||||
const isEn = pathname === "/en" || pathname.startsWith("/en/");
|
||||
const isEs = pathname === "/es" || pathname.startsWith("/es/");
|
||||
const loc = isEn ? "en" : isEs ? "es" : "pt";
|
||||
const isFr = pathname === "/fr" || pathname.startsWith("/fr/");
|
||||
const isDe = pathname === "/de" || pathname.startsWith("/de/");
|
||||
const loc = isEn ? "en" : isEs ? "es" : isFr ? "fr" : isDe ? "de" : "pt";
|
||||
|
||||
const examplesUrl = framework?.exemplos;
|
||||
|
||||
@@ -30,8 +32,12 @@ export default function CodeExample({ framework }) {
|
||||
isEn
|
||||
? `// Full examples: ${examplesUrl || "see repository"}\n`
|
||||
: isEs
|
||||
? `// Ejemplos completos: ${examplesUrl || "ver repositorio"}\n`
|
||||
: `// Exemplos completos: ${examplesUrl || "ver repositório"}\n`
|
||||
? `// Ejemplos completos: ${examplesUrl || "ver repositorio"}\n`
|
||||
: isFr
|
||||
? `// Exemples complets : ${examplesUrl || "voir le dépôt"}\n`
|
||||
: isDe
|
||||
? `// Vollständige Beispiele: ${examplesUrl || "Repository ansehen"}\n`
|
||||
: `// Exemplos completos: ${examplesUrl || "ver repositório"}\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -69,10 +75,24 @@ export default function CodeExample({ framework }) {
|
||||
isEn
|
||||
? "// The example file is empty. Refer to the official repository.\n"
|
||||
: isEs
|
||||
? "// El archivo de ejemplo está vacío. Consulte el repositorio oficial.\n"
|
||||
: "// O arquivo de exemplo está vazio. Consulte o repositório oficial.\n"
|
||||
? "// El archivo de ejemplo está vacío. Consulte el repositorio oficial.\n"
|
||||
: isFr
|
||||
? "// Le fichier d'exemple est vide. Consultez le dépôt officiel.\n"
|
||||
: isDe
|
||||
? "// Die Beispieldatei ist leer. Bitte konsultieren Sie das offizielle Repository.\n"
|
||||
: "// O arquivo de exemplo está vazio. Consulte o repositório oficial.\n"
|
||||
);
|
||||
setError(
|
||||
isEn
|
||||
? "empty file"
|
||||
: isEs
|
||||
? "archivo vacío"
|
||||
: isFr
|
||||
? "fichier vide"
|
||||
: isDe
|
||||
? "leere Datei"
|
||||
: "arquivo vazio"
|
||||
);
|
||||
setError(isEn ? "empty file" : isEs ? "archivo vacío" : "arquivo vazio");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -81,10 +101,25 @@ export default function CodeExample({ framework }) {
|
||||
isEn
|
||||
? "// Could not load example.\n// Refer to official repository for complete examples.\n"
|
||||
: isEs
|
||||
? "// No fue posible cargar el ejemplo.\n// Consulte el repositorio oficial para ver los ejemplos completos.\n"
|
||||
: "// Não foi possível carregar o exemplo.\n// Consulte o repositório oficial para exemplos completos.\n"
|
||||
? "// No fue posible cargar el ejemplo.\n// Consulte el repositorio oficial para ver los ejemplos completos.\n"
|
||||
: isFr
|
||||
? "// Impossible de charger l'exemple.\n// Consultez le dépôt officiel pour des exemples complets.\n"
|
||||
: isDe
|
||||
? "// Beispiel konnte nicht geladen werden.\n// Vollständige Beispiele finden Sie im offiziellen Repository.\n"
|
||||
: "// Não foi possível carregar o exemplo.\n// Consulte o repositório oficial para exemplos completos.\n"
|
||||
);
|
||||
setError(
|
||||
err.message ||
|
||||
(isEn
|
||||
? "load failure"
|
||||
: isEs
|
||||
? "fallo al cargar"
|
||||
: isFr
|
||||
? "échec du chargement"
|
||||
: isDe
|
||||
? "Ladefehler"
|
||||
: "falha ao carregar")
|
||||
);
|
||||
setError(err.message || (isEn ? "load failure" : isEs ? "fallo al cargar" : "falha ao carregar"));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
@@ -93,7 +128,7 @@ export default function CodeExample({ framework }) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [framework?.exemplo, framework?.exemplos, isEn, isEs, loc]);
|
||||
}, [framework?.exemplo, framework?.exemplos, isEn, isEs, isFr, isDe, loc]);
|
||||
|
||||
const lines = (sample || "").replace(/\n$/, "").split("\n");
|
||||
|
||||
@@ -110,27 +145,53 @@ export default function CodeExample({ framework }) {
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
{isEn ? "Code example" : isEs ? "Ejemplo de código" : "Exemplo de código"}
|
||||
{isEn
|
||||
? "Code example"
|
||||
: isEs
|
||||
? "Ejemplo de código"
|
||||
: isFr
|
||||
? "Exemple de code"
|
||||
: isDe
|
||||
? "Codebeispiel"
|
||||
: "Exemplo de código"}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
{isEn
|
||||
? `Starter snippet in ${framework?.linguagem}. Adjust pins and configuration for your board.`
|
||||
: isEs
|
||||
? `Fragmento de inicio en ${framework?.linguagem}. Ajuste los pines y la configuración para su placa.`
|
||||
: `Trecho de partida em ${framework?.linguagem}. Ajuste pinos e configuração à sua placa.`}
|
||||
? `Fragmento de inicio en ${framework?.linguagem}. Ajuste los pines y la configuración para su placa.`
|
||||
: isFr
|
||||
? `Extrait de départ en ${framework?.linguagem}. Adaptez les broches et la configuration à votre carte.`
|
||||
: isDe
|
||||
? `Startbeispiel in ${framework?.linguagem}. Passen Sie Pins und Konfiguration an Ihr Board an.`
|
||||
: `Trecho de partida em ${framework?.linguagem}. Ajuste pinos e configuração à sua placa.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button type="button" onClick={handleCopy} className="btn btn-ghost btn-sm">
|
||||
{copied ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
<span role="status">{copied ? (isEn ? "Copied" : isEs ? "Copiado" : "Copiado") : (isEn ? "Copy" : isEs ? "Copiar" : "Copiar")}</span>
|
||||
<span role="status">
|
||||
{copied
|
||||
? (isEn ? "Copied" : isEs ? "Copiado" : isFr ? "Copié" : isDe ? "Kopiert" : "Copiado")
|
||||
: (isEn ? "Copy" : isEs ? "Copiar" : isFr ? "Copier" : isDe ? "Kopieren" : "Copiar")}
|
||||
</span>
|
||||
</button>
|
||||
{examplesUrl && (
|
||||
<a href={examplesUrl} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
|
||||
{isEn ? "More examples" : isEs ? "Más ejemplos" : "Mais exemplos"}
|
||||
{isEn ? "More examples" : isEs ? "Más ejemplos" : isFr ? "Plus d'exemples" : isDe ? "Weitere Beispiele" : "Mais exemplos"}
|
||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only">{isEn ? " (opens in new tab)" : isEs ? " (abre en una pestaña nueva)" : " (abre em nova aba)"}</span>
|
||||
<span className="sr-only">
|
||||
{isEn
|
||||
? " (opens in new tab)"
|
||||
: isEs
|
||||
? " (abre en una pestaña nueva)"
|
||||
: isFr
|
||||
? " (ouvre un nouvel onglet)"
|
||||
: isDe
|
||||
? " (öffnet in neuem Tab)"
|
||||
: " (abre em nova aba)"}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -139,9 +200,34 @@ export default function CodeExample({ framework }) {
|
||||
<div className="code-panel mt-5" aria-busy={loading}>
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2.5 text-xs text-[#a6afbd]">
|
||||
<span className="font-mono">{framework?.nome}</span>
|
||||
<span>{loading ? (isEn ? "Loading example…" : isEs ? "Cargando ejemplo…" : "Carregando exemplo…") : framework?.linguagem}</span>
|
||||
<span>
|
||||
{loading
|
||||
? (isEn
|
||||
? "Loading example…"
|
||||
: isEs
|
||||
? "Cargando ejemplo…"
|
||||
: isFr
|
||||
? "Chargement de l'exemple…"
|
||||
: isDe
|
||||
? "Beispiel wird geladen…"
|
||||
: "Carregando exemplo…")
|
||||
: framework?.linguagem}
|
||||
</span>
|
||||
</div>
|
||||
<pre tabIndex={0} aria-label={isEn ? `Code example in ${framework?.linguagem}` : isEs ? `Código de ejemplo en ${framework?.linguagem}` : `Código de exemplo em ${framework?.linguagem}`}>
|
||||
<pre
|
||||
tabIndex={0}
|
||||
aria-label={
|
||||
isEn
|
||||
? `Code example in ${framework?.linguagem}`
|
||||
: isEs
|
||||
? `Código de ejemplo en ${framework?.linguagem}`
|
||||
: isFr
|
||||
? `Exemple de code en ${framework?.linguagem}`
|
||||
: isDe
|
||||
? `Codebeispiel in ${framework?.linguagem}`
|
||||
: `Código de exemplo em ${framework?.linguagem}`
|
||||
}
|
||||
>
|
||||
<code>
|
||||
{lines.map((line, idx) => (
|
||||
<span key={idx} className="flex">
|
||||
@@ -158,8 +244,12 @@ export default function CodeExample({ framework }) {
|
||||
{isEn
|
||||
? `Could not load local example (${error}). Use “More examples” to view official code.`
|
||||
: isEs
|
||||
? `No se pudo cargar el ejemplo local (${error}). Utilice “Más ejemplos” para ver el código oficial.`
|
||||
: `Não foi possível carregar o exemplo local (${error}). Use “Mais exemplos” para ver o código oficial.`}
|
||||
? `No se pudo cargar el ejemplo local (${error}). Utilice “Más ejemplos” para ver el código oficial.`
|
||||
: isFr
|
||||
? `Impossible de charger l'exemple local (${error}). Utilisez « Plus d'exemples » pour consulter le code officiel.`
|
||||
: isDe
|
||||
? `Lokales Beispiel konnte nicht geladen werden (${error}). Nutzen Sie „Weitere Beispiele“, um den offiziellen Code anzusehen.`
|
||||
: `Não foi possível carregar o exemplo local (${error}). Use “Mais exemplos” para ver o código oficial.`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -79,6 +79,52 @@ const FILTER_CATEGORIES_ES = [
|
||||
{ id: "Antena", name: "Antena", color: "#06b6d4" },
|
||||
];
|
||||
|
||||
const FILTER_CATEGORIES_FR = [
|
||||
{ id: "All", name: "Tous", color: "#8b5cf6" },
|
||||
{ id: "GND", name: "Masse (GND)", color: "#64748b" },
|
||||
{ id: "Power", name: "Alimentation", color: "#ef4444" },
|
||||
{ id: "GPIO", name: "GPIO", color: "#22c55e" },
|
||||
{ id: "Low Power", name: "GPIO LP", color: "#f59e0b" },
|
||||
{ id: "ADC", name: "ADC", color: "#ec4899" },
|
||||
{ id: "DAC", name: "DAC", color: "#a855f7" },
|
||||
{ id: "Touch", name: "Touch", color: "#06b6d4" },
|
||||
{ id: "UART", name: "UART", color: "#3b82f6" },
|
||||
{ id: "SPI", name: "SPI", color: "#f43f5e" },
|
||||
{ id: "USB", name: "USB", color: "#a855f7" },
|
||||
{ id: "Clock", name: "Horloge", color: "#eab308" },
|
||||
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
|
||||
{ id: "Strapping", name: "Démarrage (Boot)", color: "#dc2626" },
|
||||
{ id: "DSI", name: "Affichage (DSI)", color: "#f97316" },
|
||||
{ id: "CSI", name: "Caméra (CSI)", color: "#14b8a6" },
|
||||
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" },
|
||||
{ id: "SD", name: "Carte SD", color: "#10b981" },
|
||||
{ id: "I2C", name: "I2C", color: "#f59e0b" },
|
||||
{ id: "Antenne", name: "Antenne", color: "#06b6d4" },
|
||||
];
|
||||
|
||||
const FILTER_CATEGORIES_DE = [
|
||||
{ id: "All", name: "Alle", color: "#8b5cf6" },
|
||||
{ id: "GND", name: "Masse (GND)", color: "#64748b" },
|
||||
{ id: "Power", name: "Stromversorgung", color: "#ef4444" },
|
||||
{ id: "GPIO", name: "GPIO", color: "#22c55e" },
|
||||
{ id: "Low Power", name: "LP GPIO", color: "#f59e0b" },
|
||||
{ id: "ADC", name: "ADC", color: "#ec4899" },
|
||||
{ id: "DAC", name: "DAC", color: "#a855f7" },
|
||||
{ id: "Touch", name: "Touch", color: "#06b6d4" },
|
||||
{ id: "UART", name: "UART", color: "#3b82f6" },
|
||||
{ id: "SPI", name: "SPI", color: "#f43f5e" },
|
||||
{ id: "USB", name: "USB", color: "#a855f7" },
|
||||
{ id: "Clock", name: "Takt", color: "#eab308" },
|
||||
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
|
||||
{ id: "Strapping", name: "Bootmodus", color: "#dc2626" },
|
||||
{ id: "DSI", name: "Display (DSI)", color: "#f97316" },
|
||||
{ id: "CSI", name: "Kamera (CSI)", color: "#14b8a6" },
|
||||
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" },
|
||||
{ id: "SD", name: "SD-Karte", color: "#10b981" },
|
||||
{ id: "I2C", name: "I2C", color: "#f59e0b" },
|
||||
{ id: "Antenne", name: "Antenne", color: "#06b6d4" },
|
||||
];
|
||||
|
||||
const type_colors = {
|
||||
power: "#ef4444", // high-voltage red
|
||||
ground: "#64748b", // slate gray
|
||||
@@ -106,6 +152,8 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
const pathname = usePathname();
|
||||
const isEn = pathname?.startsWith("/en");
|
||||
const isEs = pathname?.startsWith("/es");
|
||||
const isFr = pathname?.startsWith("/fr");
|
||||
const isDe = pathname?.startsWith("/de");
|
||||
const router = useRouter();
|
||||
const pinosFiltro = useMemo(() => new Set((params.get("pinos") ?? "").split(",").filter(Boolean)), [params]);
|
||||
const motivoFiltro = params.get("motivo") ?? "";
|
||||
@@ -117,7 +165,15 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
const dialogRef = useRef(null);
|
||||
const gatilhoRef = useRef(null);
|
||||
|
||||
const filterCategories = isEn ? FILTER_CATEGORIES_EN : isEs ? FILTER_CATEGORIES_ES : FILTER_CATEGORIES;
|
||||
const filterCategories = isEn
|
||||
? FILTER_CATEGORIES_EN
|
||||
: isEs
|
||||
? FILTER_CATEGORIES_ES
|
||||
: isFr
|
||||
? FILTER_CATEGORIES_FR
|
||||
: isDe
|
||||
? FILTER_CATEGORIES_DE
|
||||
: FILTER_CATEGORIES;
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
@@ -139,7 +195,9 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
};
|
||||
|
||||
const nomeCategoria = (cat) => {
|
||||
if (cat === "Strapping") return isEn ? "Boot mode" : isEs ? "Arranque (Boot)" : "Inicialização";
|
||||
if (cat === "Strapping") {
|
||||
return isEn ? "Boot mode" : isEs ? "Arranque (Boot)" : isFr ? "Démarrage (Boot)" : isDe ? "Bootmodus" : "Inicialização";
|
||||
}
|
||||
if (isEn) {
|
||||
const found = FILTER_CATEGORIES_EN.find((f) => f.id === cat);
|
||||
if (found) return found.name;
|
||||
@@ -148,6 +206,14 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
const found = FILTER_CATEGORIES_ES.find((f) => f.id === cat);
|
||||
if (found) return found.name;
|
||||
}
|
||||
if (isFr) {
|
||||
const found = FILTER_CATEGORIES_FR.find((f) => f.id === cat);
|
||||
if (found) return found.name;
|
||||
}
|
||||
if (isDe) {
|
||||
const found = FILTER_CATEGORIES_DE.find((f) => f.id === cat);
|
||||
if (found) return found.name;
|
||||
}
|
||||
return cat;
|
||||
};
|
||||
|
||||
@@ -204,10 +270,18 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<div className="px-4 py-14 text-center">
|
||||
<Sliders className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
||||
<p className="mt-4 font-display text-xl font-semibold text-ink">
|
||||
{isEn ? "Pinout diagram unavailable" : isEs ? "Diagrama de conexiones no disponible" : "Diagrama de conexões indisponível"}
|
||||
{isEn ? "Pinout diagram unavailable" : isEs ? "Diagrama de conexiones no disponible" : isFr ? "Brochage non disponible" : isDe ? "Pinbelegungsdiagramm nicht verfügbar" : "Diagrama de conexões indisponível"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
{isEn ? "This model does not yet have a detailed pinout and bus map." : isEs ? "Este modelo todavía no dispone de un mapa detallado de conexiones y buses." : "Este modelo ainda não tem um mapa detalhado de conexões e barramentos."}
|
||||
{isEn
|
||||
? "This model does not yet have a detailed pinout and bus map."
|
||||
: isEs
|
||||
? "Este modelo todavía no dispone de un mapa detallado de conexiones y buses."
|
||||
: isFr
|
||||
? "Ce modèle ne dispose pas encore d'un plan détaillé des broches et des bus."
|
||||
: isDe
|
||||
? "Dieses Modell verfügt noch über keinen detaillierten Pinbelegungs- und Busplan."
|
||||
: "Este modelo ainda não tem um mapa detalhado de conexões e barramentos."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -219,13 +293,17 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
{isEn ? "Interactive pinout diagram" : isEs ? "Diagrama de conexiones interactivo" : "Diagrama de conexões interativo"}
|
||||
{isEn ? "Interactive pinout diagram" : isEs ? "Diagrama de conexiones interactivo" : isFr ? "Brochage interactif" : isDe ? "Interaktives Pinbelegungsdiagramm" : "Diagrama de conexões interativo"}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted">
|
||||
{isEn ? (
|
||||
`All connections and multiplexing for the ${serie.nome_completo}. Search by name or number, filter by function, and open a pin to see details.`
|
||||
) : isEs ? (
|
||||
`Todas las conexiones y la multiplexación del ${serie.nome_completo}. Busque por nombre o número, filtre por función y abra un pin para ver los detalles.`
|
||||
) : isFr ? (
|
||||
`Toutes les connexions et le multiplexage de l'${serie.nome_completo}. Recherchez par nom ou numéro, filtrez par fonction et ouvrez une broche pour voir les détails.`
|
||||
) : isDe ? (
|
||||
`Alle Anschlüsse und das Multiplexing des ${serie.nome_completo}. Suchen Sie nach Name oder Nummer, filtern Sie nach Funktion und öffnen Sie einen Pin für Details.`
|
||||
) : (
|
||||
`Todas as conexões e a multiplexação do ${serie.nome_completo}. Busque por nome ou número, filtre por função e abra um pino para ver os detalhes.`
|
||||
)}
|
||||
@@ -235,11 +313,16 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
{pinosFiltro.size > 0 && (
|
||||
<div role="status" className="callout flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-sm text-ink">
|
||||
<strong className="font-semibold">{isEn ? "Filtered by search:" : isEs ? "Filtrado por la búsqueda:" : "Filtrado pela busca:"}</strong> {motivoFiltro || (isEn ? "selected pins" : isEs ? "pines seleccionados" : "pinos selecionados")} ({pinosFiltro.size}{" "}
|
||||
{pinosFiltro.size === 1 ? (isEn ? "pin" : isEs ? "pin" : "pino") : (isEn ? "pins" : isEs ? "pines" : "pinos")})
|
||||
<strong className="font-semibold">
|
||||
{isEn ? "Filtered by search:" : isEs ? "Filtrado por la búsqueda:" : isFr ? "Filtré par la recherche :" : isDe ? "Nach Suche gefiltert:" : "Filtrado pela busca:"}
|
||||
</strong>{" "}
|
||||
{motivoFiltro || (isEn ? "selected pins" : isEs ? "pines seleccionados" : isFr ? "broches sélectionnées" : isDe ? "ausgewählte Pins" : "pinos selecionados")} ({pinosFiltro.size}{" "}
|
||||
{pinosFiltro.size === 1
|
||||
? (isEn ? "pin" : isEs ? "pin" : isFr ? "broche" : isDe ? "Pin" : "pino")
|
||||
: (isEn ? "pins" : isEs ? "pines" : isFr ? "broches" : isDe ? "Pins" : "pinos")})
|
||||
</p>
|
||||
<button type="button" onClick={mostrarTodos} className="btn btn-ghost btn-sm">
|
||||
{isEn ? "Show all pins" : isEs ? "Mostrar todos los pines" : "Mostrar todos os pinos"}
|
||||
{isEn ? "Show all pins" : isEs ? "Mostrar todos los pines" : isFr ? "Afficher toutes les broches" : isDe ? "Alle Pins anzeigen" : "Mostrar todos os pinos"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -247,45 +330,59 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<div className="relative flex-1">
|
||||
<label htmlFor="busca-pino" className="sr-only">
|
||||
{isEn ? "Search connection by name or number" : isEs ? "Buscar conexión por nombre o número" : "Buscar conexão por nome ou número"}
|
||||
{isEn ? "Search connection by name or number" : isEs ? "Buscar conexión por nombre o número" : isFr ? "Rechercher une connexion par nom ou numéro" : isDe ? "Anschluss nach Name oder Nummer suchen" : "Buscar conexão por nome ou número"}
|
||||
</label>
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
|
||||
<input
|
||||
id="busca-pino"
|
||||
type="search"
|
||||
placeholder={isEn ? "Search pin (e.g.: GPIO1, TXD, XTAL)" : isEs ? "Buscar pin (ej.: GPIO1, TXD, XTAL)" : "Buscar pino (ex.: GPIO1, TXD, XTAL)"}
|
||||
placeholder={
|
||||
isEn
|
||||
? "Search pin (e.g.: GPIO1, TXD, XTAL)"
|
||||
: isEs
|
||||
? "Buscar pin (ej.: GPIO1, TXD, XTAL)"
|
||||
: isFr
|
||||
? "Rechercher une broche (ex. : GPIO1, TXD, XTAL)"
|
||||
: isDe
|
||||
? "Pin suchen (z. B.: GPIO1, TXD, XTAL)"
|
||||
: "Buscar pino (ex.: GPIO1, TXD, XTAL)"
|
||||
}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="field pl-11 pr-4 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div role="group" aria-label={isEn ? "Display mode" : isEs ? "Modo de visualización" : "Modo de exibição"} className="flex gap-2">
|
||||
<div role="group" aria-label={isEn ? "Display mode" : isEs ? "Modo de visualización" : isFr ? "Mode d'affichage" : isDe ? "Anzeigemodus" : "Modo de exibição"} className="flex gap-2">
|
||||
<button type="button" onClick={() => setViewMode("grid")} aria-pressed={viewMode === "grid"} className="chip-btn">
|
||||
<Grid className="h-4 w-4" aria-hidden="true" />
|
||||
{isEn ? "Grid" : isEs ? "Cuadrícula" : "Grade"}
|
||||
{isEn ? "Grid" : isEs ? "Cuadrícula" : isFr ? "Grille" : isDe ? "Raster" : "Grade"}
|
||||
</button>
|
||||
<button type="button" onClick={() => setViewMode("list")} aria-pressed={viewMode === "list"} className="chip-btn">
|
||||
<List className="h-4 w-4" aria-hidden="true" />
|
||||
{isEn ? "List" : isEs ? "Lista" : "Lista"}
|
||||
{isEn ? "List" : isEs ? "Lista" : isFr ? "Liste" : isDe ? "Liste" : "Lista"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold text-ink">{isEn ? "Pin function" : isEs ? "Función del pin" : "Função do pino"}</h3>
|
||||
<h3 className="text-sm font-semibold text-ink">{isEn ? "Pin function" : isEs ? "Función del pin" : isFr ? "Fonction de la broche" : isDe ? "Pin-Funktion" : "Função do pino"}</h3>
|
||||
<p role="status" aria-live="polite" className="text-sm text-muted">
|
||||
{isEn ? (
|
||||
`${visibleConnections.length} of ${connections.length} pins`
|
||||
) : isEs ? (
|
||||
`${visibleConnections.length} de ${connections.length} pines`
|
||||
) : isFr ? (
|
||||
`${visibleConnections.length} sur ${connections.length} broches`
|
||||
) : isDe ? (
|
||||
`${visibleConnections.length} von ${connections.length} Pins`
|
||||
) : (
|
||||
`${visibleConnections.length} de ${connections.length} pinos`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div role="group" aria-label={isEn ? "Filter by function" : isEs ? "Filtrar por función" : "Filtrar por função"} className="mt-3 flex flex-wrap gap-2">
|
||||
<div role="group" aria-label={isEn ? "Filter by function" : isEs ? "Filtrar por función" : isFr ? "Filtrer par fonction" : isDe ? "Nach Funktion filtern" : "Filtrar por função"} className="mt-3 flex flex-wrap gap-2">
|
||||
{filterCategories.map((filter) => {
|
||||
const isActive = activeFilter === filter.id;
|
||||
const count = connections.filter((c) => (filter.id === "All" ? true : c.categorias.includes(filter.id))).length;
|
||||
@@ -304,10 +401,20 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
{visibleConnections.length === 0 ? (
|
||||
<div className="rounded-2xl border border-dashed border-line-strong px-6 py-12 text-center">
|
||||
<Search className="mx-auto h-8 w-8 text-muted" aria-hidden="true" />
|
||||
<p className="mt-3 font-semibold text-ink">{isEn ? "No connections found" : isEs ? "No se encontraron conexiones" : "Nenhuma conexão encontrada"}</p>
|
||||
<p className="mt-1 text-sm text-muted">{isEn ? "Adjust your search or return to all functions." : isEs ? "Ajuste la búsqueda o vuelva a todas las funciones." : "Ajuste a busca ou volte para todas as funções."}</p>
|
||||
<p className="mt-3 font-semibold text-ink">{isEn ? "No connections found" : isEs ? "No se encontraron conexiones" : isFr ? "Aucune connexion trouvée" : isDe ? "Keine Anschlüsse gefunden" : "Nenhuma conexão encontrada"}</p>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
{isEn
|
||||
? "Adjust your search or return to all functions."
|
||||
: isEs
|
||||
? "Ajuste la búsqueda o vuelva a todas las funciones."
|
||||
: isFr
|
||||
? "Ajustez votre recherche ou revenez à toutes les fonctions."
|
||||
: isDe
|
||||
? "Passen Sie Ihre Suche an oder kehren Sie zu allen Funktionen zurück."
|
||||
: "Ajuste a busca ou volte para todas as funções."}
|
||||
</p>
|
||||
<button type="button" onClick={() => { setActiveFilter("All"); setSearchQuery(""); if (pinosFiltro.size) mostrarTodos(); }} className="btn btn-primary btn-sm mt-5">
|
||||
{isEn ? "Clear search and filters" : isEs ? "Limpiar búsqueda y filtros" : "Limpar busca e filtros"}
|
||||
{isEn ? "Clear search and filters" : isEs ? "Limpiar búsqueda y filtros" : isFr ? "Effacer la recherche et les filtres" : isDe ? "Suche und Filter zurücksetzen" : "Limpar busca e filtros"}
|
||||
</button>
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
@@ -320,7 +427,17 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
type="button"
|
||||
onClick={(e) => abrir(connection, e)}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={isEn ? `Pin ${connection.nome}, number ${connection.numero}${connection.avisos ? ", with usage restrictions" : ""}` : isEs ? `Pin ${connection.nome}, número ${connection.numero}${connection.avisos ? ", con restricciones de uso" : ""}` : `Pino ${connection.nome}, número ${connection.numero}${connection.avisos ? ", com restrições de uso" : ""}`}
|
||||
aria-label={
|
||||
isEn
|
||||
? `Pin ${connection.nome}, number ${connection.numero}${connection.avisos ? ", with usage restrictions" : ""}`
|
||||
: isEs
|
||||
? `Pin ${connection.nome}, número ${connection.numero}${connection.avisos ? ", con restricciones de uso" : ""}`
|
||||
: isFr
|
||||
? `Broche ${connection.nome}, numéro ${connection.numero}${connection.avisos ? ", avec restrictions d'utilisation" : ""}`
|
||||
: isDe
|
||||
? `Pin ${connection.nome}, Nummer ${connection.numero}${connection.avisos ? ", mit Nutzungseinschränkungen" : ""}`
|
||||
: `Pino ${connection.nome}, número ${connection.numero}${connection.avisos ? ", com restrições de uso" : ""}`
|
||||
}
|
||||
className="flex w-full flex-col items-center rounded-xl border border-line bg-surface p-3 transition-colors hover:bg-surface-2"
|
||||
>
|
||||
<span
|
||||
@@ -349,14 +466,22 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<div className="overflow-x-auto rounded-xl border border-line">
|
||||
<table className="w-full min-w-[40rem] border-collapse text-left text-sm">
|
||||
<caption className="sr-only">
|
||||
{isEn ? `Connections of ${serie.nome_completo}` : isEs ? `Conexiones del ${serie.nome_completo}` : `Conexões do ${serie.nome_completo}`}
|
||||
{isEn
|
||||
? `Connections of ${serie.nome_completo}`
|
||||
: isEs
|
||||
? `Conexiones del ${serie.nome_completo}`
|
||||
: isFr
|
||||
? `Connexions de l'${serie.nome_completo}`
|
||||
: isDe
|
||||
? `Anschlüsse des ${serie.nome_completo}`
|
||||
: `Conexões do ${serie.nome_completo}`}
|
||||
</caption>
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-surface-2 text-muted">
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Pin" : isEs ? "Pin" : "Pino"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Name" : isEs ? "Nombre" : "Nome"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Functions" : isEs ? "Funciones" : "Funções"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Usage" : isEs ? "Uso" : "Uso"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Pin" : isEs ? "Pin" : isFr ? "Broche" : isDe ? "Pin" : "Pino"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Name" : isEs ? "Nombre" : isFr ? "Nom" : isDe ? "Name" : "Nome"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Functions" : isEs ? "Funciones" : isFr ? "Fonctions" : isDe ? "Funktionen" : "Funções"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Usage" : isEs ? "Uso" : isFr ? "Utilisation" : isDe ? "Nutzung" : "Uso"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-line">
|
||||
@@ -369,7 +494,17 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
type="button"
|
||||
onClick={(e) => abrir(connection, e)}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={isEn ? `Open pin details for ${connection.nome}, number ${connection.numero}` : isEs ? `Abrir detalles del pin ${connection.nome}, número ${connection.numero}` : `Abrir detalhes do pino ${connection.nome}, número ${connection.numero}`}
|
||||
aria-label={
|
||||
isEn
|
||||
? `Open pin details for ${connection.nome}, number ${connection.numero}`
|
||||
: isEs
|
||||
? `Abrir detalles del pin ${connection.nome}, número ${connection.numero}`
|
||||
: isFr
|
||||
? `Ouvrir les détails de la broche ${connection.nome}, numéro ${connection.numero}`
|
||||
: isDe
|
||||
? `Pindetails für ${connection.nome}, Nummer ${connection.numero} öffnen`
|
||||
: `Abrir detalhes do pino ${connection.nome}, número ${connection.numero}`
|
||||
}
|
||||
className="grid h-11 w-11 place-items-center rounded-full border-2 bg-surface text-sm font-semibold tabular-nums text-ink hover:bg-surface-2"
|
||||
style={{ borderColor: baseColor }}
|
||||
>
|
||||
@@ -384,12 +519,12 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
{connection.avisos ? (
|
||||
<span className="inline-flex items-center gap-1.5 font-medium text-danger">
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
{isEn ? "Restricted" : isEs ? "Restringido" : "Restrito"}
|
||||
{isEn ? "Restricted" : isEs ? "Restringido" : isFr ? "Restreint" : isDe ? "Eingeschränkt" : "Restrito"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 text-ok">
|
||||
<CheckCircle className="h-4 w-4" aria-hidden="true" />
|
||||
{isEn ? "Free" : isEs ? "Libre" : "Livre"}
|
||||
{isEn ? "Free" : isEs ? "Libre" : isFr ? "Libre" : isDe ? "Frei" : "Livre"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -405,7 +540,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<div className="callout callout-warn flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
|
||||
<div>
|
||||
<p className="font-semibold text-ink">{isEn ? "Pinout notes" : isEs ? "Notas sobre el patillaje" : "Notas sobre a pinagem"}</p>
|
||||
<p className="font-semibold text-ink">{isEn ? "Pinout notes" : isEs ? "Notas sobre el patillaje" : isFr ? "Remarques sur le brochage" : isDe ? "Hinweise zur Pinbelegung" : "Notas sobre a pinagem"}</p>
|
||||
<ul className="mt-1 list-disc space-y-1 pl-5">
|
||||
{isEn ? (
|
||||
<>
|
||||
@@ -417,6 +552,16 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<li>Los pines con icono de advertencia tienen un papel especial en el arranque o limitaciones analógicas. Abra el pin para ver los detalles.</li>
|
||||
<li>Aísle las pistas táctiles capacitivas del ruido de RF acoplado.</li>
|
||||
</>
|
||||
) : isFr ? (
|
||||
<>
|
||||
<li>Les broches signalées par une icône d'avertissement ont un rôle spécial au démarrage ou des restrictions analogiques. Ouvrez la broche pour voir les détails.</li>
|
||||
<li>Isolez les pistes tactiles capacitives des bruits RF couplés.</li>
|
||||
</>
|
||||
) : isDe ? (
|
||||
<>
|
||||
<li>Pins mit einem Warnsymbol spielen eine besondere Rolle beim Booten oder unterliegen analogen Einschränkungen. Öffnen Sie den Pin für Details.</li>
|
||||
<li>Isolieren Sie kapazitive Touch-Leitungen von eingekoppeltem HF-Rauschen.</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>Pinos com o ícone de alerta têm papel especial no boot ou limitações analógicas. Abra o pino para ver os detalhes.</li>
|
||||
@@ -451,11 +596,24 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
{selectedConnection.nome}
|
||||
</h3>
|
||||
<p className="text-sm text-muted">
|
||||
{isEn ? `Physical pin ${selectedConnection.numero}` : isEs ? `Pin físico ${selectedConnection.numero}` : `Pino físico ${selectedConnection.numero}`}
|
||||
{isEn
|
||||
? `Physical pin ${selectedConnection.numero}`
|
||||
: isEs
|
||||
? `Pin físico ${selectedConnection.numero}`
|
||||
: isFr
|
||||
? `Broche physique ${selectedConnection.numero}`
|
||||
: isDe
|
||||
? `Physischer Pin ${selectedConnection.numero}`
|
||||
: `Pino físico ${selectedConnection.numero}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick={fechar} aria-label={isEn ? "Close pin details" : isEs ? "Cerrar detalles del pin" : "Fechar detalhes do pino"} className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink">
|
||||
<button
|
||||
type="button"
|
||||
onClick={fechar}
|
||||
aria-label={isEn ? "Close pin details" : isEs ? "Cerrar detalles del pin" : isFr ? "Fermer les détails de la broche" : isDe ? "Pindetails schließen" : "Fechar detalhes do pino"}
|
||||
className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink"
|
||||
>
|
||||
<X className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -463,7 +621,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-5 sm:p-7">
|
||||
{selectedConnection.categorias.length > 0 && (
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-ink">{isEn ? "Categories" : isEs ? "Categorías" : "Categorias"}</h4>
|
||||
<h4 className="text-sm font-semibold text-ink">{isEn ? "Categories" : isEs ? "Categorías" : isFr ? "Catégories" : isDe ? "Kategorien" : "Categorias"}</h4>
|
||||
<ul className="mt-3 flex flex-wrap gap-2">
|
||||
{selectedConnection.categorias.map((cat) => (
|
||||
<li key={cat} className="chip">
|
||||
@@ -479,7 +637,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<section>
|
||||
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
|
||||
<Zap className="h-4 w-4 text-danger" aria-hidden="true" />
|
||||
{isEn ? "Power domain" : isEs ? "Dominio de alimentación" : "Domínio de alimentação"}
|
||||
{isEn ? "Power domain" : isEs ? "Dominio de alimentación" : isFr ? "Domaine d'alimentation" : isDe ? "Spannungsbereich" : "Domínio de alimentação"}
|
||||
</h4>
|
||||
<ul className="tile mt-3 space-y-2">
|
||||
{selectedConnection.alimentacao.map((source) => (
|
||||
@@ -490,7 +648,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-ink">{isEn ? "Available functions" : isEs ? "Funciones disponibles" : "Funções disponíveis"}</h4>
|
||||
<h4 className="text-sm font-semibold text-ink">{isEn ? "Available functions" : isEs ? "Funciones disponibles" : isFr ? "Fonctions disponibles" : isDe ? "Verfügbare Funktionen" : "Funções disponíveis"}</h4>
|
||||
<ul className="tile mt-3 grid gap-2.5 sm:grid-cols-2">
|
||||
{selectedConnection.funcoes.map((func) => (
|
||||
<li key={func} className="flex items-center gap-2.5 text-sm text-ink-soft">
|
||||
@@ -505,7 +663,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<section className="callout callout-danger">
|
||||
<h4 className="flex items-center gap-2 font-semibold text-ink">
|
||||
<AlertTriangle className="h-4 w-4 text-danger" aria-hidden="true" />
|
||||
{isEn ? "Engineering notes" : isEs ? "Notas de ingeniería" : "Avisos de engenharia"}
|
||||
{isEn ? "Engineering notes" : isEs ? "Notas de ingeniería" : isFr ? "Notes d'ingénierie" : isDe ? "Entwicklungshinweise" : "Avisos de engenharia"}
|
||||
</h4>
|
||||
<ul className="mt-2 list-disc space-y-1.5 pl-5">
|
||||
{selectedConnection.avisos.map((aviso) => (
|
||||
@@ -518,7 +676,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
|
||||
<div className="border-t border-line p-4 sm:px-7">
|
||||
<button type="button" onClick={fechar} className="btn btn-primary w-full">
|
||||
{isEn ? "Close" : isEs ? "Cerrar" : "Fechar"}
|
||||
{isEn ? "Close" : isEs ? "Cerrar" : isFr ? "Fermer" : isDe ? "Schließen" : "Fechar"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+149
-7
@@ -96,6 +96,64 @@ const COLUMNS_ES = [
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMNS_FR = [
|
||||
{
|
||||
title: "Explorer",
|
||||
links: [
|
||||
{ href: "/series", label: "Séries ESP32" },
|
||||
{ href: "/comparacao", label: "Comparateur" },
|
||||
{ href: "/catalogo", label: "Catalogue de cartes" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Outils",
|
||||
links: [
|
||||
{ href: "/seletor", label: "Sélecteur intelligent" },
|
||||
{ href: "/diagnostico", label: "Diagnostic" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/frameworks", label: "Frameworks et SDKs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Projet",
|
||||
links: [
|
||||
{ href: "/sobre", label: "À propos d'ESPDocs" },
|
||||
{ href: "/glossario", label: "Glossaire" },
|
||||
{ href: SITE.repositorio, label: "Dépôt", external: true },
|
||||
{ href: `${SITE.repositorio}/issues`, label: "Signaler un problème", external: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMNS_DE = [
|
||||
{
|
||||
title: "Erkunden",
|
||||
links: [
|
||||
{ href: "/series", label: "ESP32-Serien" },
|
||||
{ href: "/comparacao", label: "Vergleich" },
|
||||
{ href: "/catalogo", label: "Board-Katalog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Werkzeuge",
|
||||
links: [
|
||||
{ href: "/seletor", label: "Intelligente Auswahl" },
|
||||
{ href: "/diagnostico", label: "Diagnose" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/frameworks", label: "Frameworks & SDKs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Projekt",
|
||||
links: [
|
||||
{ href: "/sobre", label: "Über ESPDocs" },
|
||||
{ href: "/glossario", label: "Glossar" },
|
||||
{ href: SITE.repositorio, label: "Repository", external: true },
|
||||
{ href: `${SITE.repositorio}/issues`, label: "Problem melden", external: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const pathname = usePathname() || "";
|
||||
const year = new Date().getFullYear();
|
||||
@@ -105,6 +163,8 @@ export default function Footer() {
|
||||
const isPt = currentLocale === "pt";
|
||||
const isEn = currentLocale === "en";
|
||||
const isEs = currentLocale === "es";
|
||||
const isFr = currentLocale === "fr";
|
||||
const isDe = currentLocale === "de";
|
||||
|
||||
const getLinkHref = (href) => getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
|
||||
|
||||
@@ -115,14 +175,45 @@ export default function Footer() {
|
||||
}))
|
||||
: isEs
|
||||
? COLUMNS_ES
|
||||
: isFr
|
||||
? COLUMNS_FR
|
||||
: isDe
|
||||
? COLUMNS_DE
|
||||
: COLUMNS_EN;
|
||||
|
||||
return (
|
||||
<footer className="mt-20 border-t border-line md:mt-28" aria-label={isPt ? "Rodapé do ESPDocs" : isEs ? "Pie de página de ESPDocs" : "ESPDocs footer"}>
|
||||
<footer
|
||||
className="mt-20 border-t border-line md:mt-28"
|
||||
aria-label={
|
||||
isPt
|
||||
? "Rodapé do ESPDocs"
|
||||
: isEs
|
||||
? "Pie de página de ESPDocs"
|
||||
: isFr
|
||||
? "Pied de page d'ESPDocs"
|
||||
: isDe
|
||||
? "ESPDocs-Fußzeile"
|
||||
: "ESPDocs footer"
|
||||
}
|
||||
>
|
||||
<div className="shell py-14 md:py-20">
|
||||
<div className="grid gap-12 lg:grid-cols-[1.1fr_2fr]">
|
||||
<div>
|
||||
<Link href={getLinkHref("/")} className="inline-flex min-h-11 items-center gap-2.5" aria-label={isPt ? "ESPDocs, página inicial" : isEs ? "ESPDocs, página de inicio" : "ESPDocs, homepage"}>
|
||||
<Link
|
||||
href={getLinkHref("/")}
|
||||
className="inline-flex min-h-11 items-center gap-2.5"
|
||||
aria-label={
|
||||
isPt
|
||||
? "ESPDocs, página inicial"
|
||||
: isEs
|
||||
? "ESPDocs, página de inicio"
|
||||
: isFr
|
||||
? "ESPDocs, page d'accueil"
|
||||
: isDe
|
||||
? "ESPDocs, Startseite"
|
||||
: "ESPDocs, homepage"
|
||||
}
|
||||
>
|
||||
<Logo className="h-7 w-7" />
|
||||
<span className="font-display text-2xl font-semibold tracking-[-0.045em] text-ink">ESPDocs</span>
|
||||
</Link>
|
||||
@@ -137,6 +228,16 @@ export default function Footer() {
|
||||
Documentación independiente del ecosistema ESP32.
|
||||
Datos públicos, curaduría editorial y enlaces a las fuentes oficiales.
|
||||
</>
|
||||
) : isFr ? (
|
||||
<>
|
||||
Documentation indépendante de l'écosystème ESP32.
|
||||
Données publiques, sélection éditoriale et liens vers les sources officielles.
|
||||
</>
|
||||
) : isDe ? (
|
||||
<>
|
||||
Unabhängige Dokumentation des ESP32-Ökosystems.
|
||||
Öffentliche Daten, redaktionelle Betreuung und Links zu offiziellen Quellen.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Independent documentation of the ESP32 ecosystem.
|
||||
@@ -211,7 +312,7 @@ export default function Footer() {
|
||||
))}
|
||||
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-ink">{isPt ? "Autor" : isEs ? "Autor" : "Author"}</h2>
|
||||
<h2 className="text-sm font-semibold text-ink">{isPt ? "Autor" : isEs ? "Autor" : isFr ? "Auteur" : isDe ? "Autor" : "Author"}</h2>
|
||||
<p className="mt-4 text-sm leading-relaxed text-muted">
|
||||
Lucas Rayan Guerra
|
||||
<br />
|
||||
@@ -222,7 +323,17 @@ export default function Footer() {
|
||||
href={SITE.repositorio}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={isPt ? "Gitea do projeto (abre em nova aba)" : isEs ? "Gitea del proyecto (abre en una pestaña nueva)" : "Project Gitea (opens in new tab)"}
|
||||
aria-label={
|
||||
isPt
|
||||
? "Gitea do projeto (abre em nova aba)"
|
||||
: isEs
|
||||
? "Gitea del proyecto (abre en una pestaña nueva)"
|
||||
: isFr
|
||||
? "Gitea du projet (ouvre un nouvel onglet)"
|
||||
: isDe
|
||||
? "Projekt-Gitea (öffnet in neuem Tab)"
|
||||
: "Project Gitea (opens in new tab)"
|
||||
}
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
|
||||
>
|
||||
<GiteaMark className="h-4 w-4" />
|
||||
@@ -231,7 +342,17 @@ export default function Footer() {
|
||||
href={SITE.autorLinkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={isPt ? "LinkedIn do autor (abre em nova aba)" : isEs ? "LinkedIn del autor (abre en una pestaña nueva)" : "Author LinkedIn (opens in new tab)"}
|
||||
aria-label={
|
||||
isPt
|
||||
? "LinkedIn do autor (abre em nova aba)"
|
||||
: isEs
|
||||
? "LinkedIn del autor (abre en una pestaña nueva)"
|
||||
: isFr
|
||||
? "LinkedIn de l'auteur (ouvre un nouvel onglet)"
|
||||
: isDe
|
||||
? "LinkedIn des Autors (öffnet in neuem Tab)"
|
||||
: "Author LinkedIn (opens in new tab)"
|
||||
}
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
@@ -244,7 +365,18 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="mt-14 flex flex-col gap-4 border-t border-line pt-6 text-xs leading-relaxed text-faint md:flex-row md:items-center md:justify-between">
|
||||
<p>© {year} {isPt ? "ESPDocs. Projeto independente, sem afiliação com a Espressif Systems." : isEs ? "ESPDocs. Proyecto independiente, sin afiliación con Espressif Systems." : "ESPDocs. Independent project, not affiliated with Espressif Systems."}</p>
|
||||
<p>
|
||||
© {year}{" "}
|
||||
{isPt
|
||||
? "ESPDocs. Projeto independente, sem afiliação com a Espressif Systems."
|
||||
: isEs
|
||||
? "ESPDocs. Proyecto independiente, sin afiliación con Espressif Systems."
|
||||
: isFr
|
||||
? "ESPDocs. Projet indépendant, sans affiliation avec Espressif Systems."
|
||||
: isDe
|
||||
? "ESPDocs. Unabhängiges Projekt, nicht verbunden mit Espressif Systems."
|
||||
: "ESPDocs. Independent project, not affiliated with Espressif Systems."}
|
||||
</p>
|
||||
<p className="flex flex-wrap items-center gap-x-5 gap-y-2">
|
||||
<a href={SITE.blog} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 transition-colors hover:text-accent">
|
||||
Ciência Embarcada
|
||||
@@ -254,7 +386,17 @@ export default function Footer() {
|
||||
lucasrguerra.dev.br
|
||||
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
<span>{isPt ? "Fontes oficiais: Espressif Systems." : isEs ? "Fuentes oficiales: Espressif Systems." : "Official sources: Espressif Systems."}</span>
|
||||
<span>
|
||||
{isPt
|
||||
? "Fontes oficiais: Espressif Systems."
|
||||
: isEs
|
||||
? "Fuentes oficiales: Espressif Systems."
|
||||
: isFr
|
||||
? "Sources officielles : Espressif Systems."
|
||||
: isDe
|
||||
? "Offizielle Quellen: Espressif Systems."
|
||||
: "Official sources: Espressif Systems."}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+206
-17
@@ -118,6 +118,66 @@ const MENUS_ES = [
|
||||
},
|
||||
];
|
||||
|
||||
const MENUS_FR = [
|
||||
{
|
||||
id: "hardware",
|
||||
label: "Matériel",
|
||||
items: [
|
||||
{ href: "/series", label: "Séries ESP32", desc: "12 puces, brochages et spécifications", icon: Cpu },
|
||||
{ href: "/comparacao", label: "Comparateur", desc: "Jusqu'à quatre séries côte à côte", icon: Scale },
|
||||
{ href: "/catalogo", label: "Catalogue de cartes", desc: "DevKits, broches et où acheter", icon: Box },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "tools",
|
||||
label: "Outils",
|
||||
items: [
|
||||
{ href: "/seletor", label: "Sélecteur intelligent", desc: "La puce idéale pour votre projet", icon: SlidersHorizontal },
|
||||
{ href: "/diagnostico", label: "Diagnostic", desc: "Brownout, bootloop, WDT et plus", icon: AlertTriangle },
|
||||
{ href: "/componentes", label: "Component Registry", desc: "Pilotes et manifestes idf_component.yml", icon: Search },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "library",
|
||||
label: "Bibliothèque",
|
||||
items: [
|
||||
{ href: "/frameworks", label: "Frameworks et SDKs", desc: "ESP-IDF, Arduino, Rust et Matter", icon: BookOpen },
|
||||
{ href: "/glossario", label: "Glossaire technique", desc: "Les termes des fiches techniques", icon: BookA },
|
||||
{ href: "/sobre", label: "À propos du projet", desc: "Auteur, méthode et open source", icon: Sparkles },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MENUS_DE = [
|
||||
{
|
||||
id: "hardware",
|
||||
label: "Hardware",
|
||||
items: [
|
||||
{ href: "/series", label: "ESP32-Serien", desc: "12 Chips, Pinbelegungen und Spezifikationen", icon: Cpu },
|
||||
{ href: "/comparacao", label: "Vergleich", desc: "Bis zu vier Serien nebeneinander", icon: Scale },
|
||||
{ href: "/catalogo", label: "Board-Katalog", desc: "DevKits, Pins und Bezugsquellen", icon: Box },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "tools",
|
||||
label: "Werkzeuge",
|
||||
items: [
|
||||
{ href: "/seletor", label: "Intelligente Auswahl", desc: "Der richtige Chip für Ihr Projekt", icon: SlidersHorizontal },
|
||||
{ href: "/diagnostico", label: "Diagnose", desc: "Brownout, Bootloops, WDT und mehr", icon: AlertTriangle },
|
||||
{ href: "/componentes", label: "Component Registry", desc: "Treiber und idf_component.yml-Manifeste", icon: Search },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "library",
|
||||
label: "Bibliothek",
|
||||
items: [
|
||||
{ href: "/frameworks", label: "Frameworks & SDKs", desc: "ESP-IDF, Arduino, Rust und Matter", icon: BookOpen },
|
||||
{ href: "/glossario", label: "Technisches Glossar", desc: "Begriffe aus den Datenblättern", icon: BookA },
|
||||
{ href: "/sobre", label: "Über das Projekt", desc: "Autorschaft, Methode und Open Source", icon: Sparkles },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MOBILE_LINKS_PT = [
|
||||
{ href: "/series", label: "Séries ESP32" },
|
||||
{ href: "/comparacao", label: "Comparador" },
|
||||
@@ -155,6 +215,30 @@ const MOBILE_LINKS_ES = [
|
||||
{ href: "/sobre", label: "Acerca de" },
|
||||
];
|
||||
|
||||
const MOBILE_LINKS_FR = [
|
||||
{ href: "/series", label: "Séries ESP32" },
|
||||
{ href: "/comparacao", label: "Comparateur" },
|
||||
{ href: "/seletor", label: "Sélecteur intelligent" },
|
||||
{ href: "/diagnostico", label: "Diagnostic" },
|
||||
{ href: "/frameworks", label: "Frameworks et SDKs" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/glossario", label: "Glossaire" },
|
||||
{ href: "/catalogo", label: "Catalogue de cartes" },
|
||||
{ href: "/sobre", label: "À propos" },
|
||||
];
|
||||
|
||||
const MOBILE_LINKS_DE = [
|
||||
{ href: "/series", label: "ESP32-Serien" },
|
||||
{ href: "/comparacao", label: "Vergleich" },
|
||||
{ href: "/seletor", label: "Intelligente Auswahl" },
|
||||
{ href: "/diagnostico", label: "Diagnose" },
|
||||
{ href: "/frameworks", label: "Frameworks & SDKs" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/glossario", label: "Glossar" },
|
||||
{ href: "/catalogo", label: "Board-Katalog" },
|
||||
{ href: "/sobre", label: "Über uns" },
|
||||
];
|
||||
|
||||
function isActivePath(pathname, href, currentLocale) {
|
||||
const localized = getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
|
||||
if (href === "/" || localized === `/${currentLocale}/` || localized === `/${currentLocale}`) {
|
||||
@@ -177,6 +261,8 @@ export default function Header() {
|
||||
const isPt = currentLocale === "pt";
|
||||
const isEn = currentLocale === "en";
|
||||
const isEs = currentLocale === "es";
|
||||
const isFr = currentLocale === "fr";
|
||||
const isDe = currentLocale === "de";
|
||||
|
||||
const getLinkHref = (href) => getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
|
||||
|
||||
@@ -216,19 +302,55 @@ export default function Header() {
|
||||
};
|
||||
}, [drawerOpen]);
|
||||
|
||||
const visibleMobileLinks = isPt ? MOBILE_LINKS_PT : isEs ? MOBILE_LINKS_ES : MOBILE_LINKS_EN;
|
||||
const MENUS = isPt ? MENUS_PT : isEs ? MENUS_ES : MENUS_EN;
|
||||
const visibleMobileLinks = isPt
|
||||
? MOBILE_LINKS_PT
|
||||
: isEs
|
||||
? MOBILE_LINKS_ES
|
||||
: isFr
|
||||
? MOBILE_LINKS_FR
|
||||
: isDe
|
||||
? MOBILE_LINKS_DE
|
||||
: MOBILE_LINKS_EN;
|
||||
const MENUS = isPt ? MENUS_PT : isEs ? MENUS_ES : isFr ? MENUS_FR : isDe ? MENUS_DE : MENUS_EN;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 pt-3 sm:pt-4">
|
||||
<div className="shell">
|
||||
<div className="nav-pill flex h-16 items-center justify-between gap-3 rounded-full px-3 sm:px-4">
|
||||
<Link href={getLinkHref("/")} className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2" aria-label={isPt ? "ESPDocs, página inicial" : isEs ? "ESPDocs, página de inicio" : "ESPDocs, homepage"}>
|
||||
<Link
|
||||
href={getLinkHref("/")}
|
||||
className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2"
|
||||
aria-label={
|
||||
isPt
|
||||
? "ESPDocs, página inicial"
|
||||
: isEs
|
||||
? "ESPDocs, página de inicio"
|
||||
: isFr
|
||||
? "ESPDocs, page d'accueil"
|
||||
: isDe
|
||||
? "ESPDocs, Startseite"
|
||||
: "ESPDocs, homepage"
|
||||
}
|
||||
>
|
||||
<Logo className="h-[30px] w-[30px]" />
|
||||
<span className="font-display text-[1.3rem] font-semibold leading-none tracking-[-0.045em] text-ink">ESPDocs</span>
|
||||
</Link>
|
||||
|
||||
<nav ref={navRef} className="hidden items-center gap-0.5 lg:flex" aria-label={isPt ? "Navegação principal" : isEs ? "Navegación principal" : "Main navigation"}>
|
||||
<nav
|
||||
ref={navRef}
|
||||
className="hidden items-center gap-0.5 lg:flex"
|
||||
aria-label={
|
||||
isPt
|
||||
? "Navegação principal"
|
||||
: isEs
|
||||
? "Navegación principal"
|
||||
: isFr
|
||||
? "Navigation principale"
|
||||
: isDe
|
||||
? "Hauptnavigation"
|
||||
: "Main navigation"
|
||||
}
|
||||
>
|
||||
{MENUS.map((menu) => {
|
||||
const active = menu.items.some((item) => isActivePath(pathname, item.href, currentLocale));
|
||||
const expanded = openMenu === menu.id;
|
||||
@@ -281,23 +403,33 @@ export default function Header() {
|
||||
</Link>
|
||||
)}
|
||||
<Link href={getLinkHref("/sobre")} className={`nav-link ${isActivePath(pathname, "/sobre", currentLocale) ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre", currentLocale) ? "page" : undefined}>
|
||||
{isPt ? "Sobre" : isEs ? "Acerca de" : "About"}
|
||||
{isPt ? "Sobre" : isEs ? "Acerca de" : isFr ? "À propos" : isDe ? "Über uns" : "About"}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Link href={getLinkHref("/seletor")} className="btn btn-primary btn-sm hidden lg:inline-flex">
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
{isPt ? "Selecionar chip" : isEs ? "Seleccionar chip" : "Select chip"}
|
||||
{isPt ? "Selecionar chip" : isEs ? "Seleccionar chip" : isFr ? "Choisir une puce" : isDe ? "Chip auswählen" : "Select chip"}
|
||||
</Link>
|
||||
<SearchDialog />
|
||||
<LanguageSelector />
|
||||
<ThemeToggle isPt={isPt} isEs={isEs} />
|
||||
<ThemeToggle isPt={isPt} isEs={isEs} isFr={isFr} isDe={isDe} />
|
||||
<button
|
||||
ref={menuButtonRef}
|
||||
type="button"
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2 lg:hidden"
|
||||
aria-label={isPt ? "Abrir menu de navegação" : isEs ? "Abrir menú de navegación" : "Open navigation menu"}
|
||||
aria-label={
|
||||
isPt
|
||||
? "Abrir menu de navegação"
|
||||
: isEs
|
||||
? "Abrir menú de navegación"
|
||||
: isFr
|
||||
? "Ouvrir le menu de navigation"
|
||||
: isDe
|
||||
? "Navigationsmenü öffnen"
|
||||
: "Open navigation menu"
|
||||
}
|
||||
aria-expanded={drawerOpen}
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
@@ -320,7 +452,17 @@ export default function Header() {
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={isPt ? "Menu de navegação" : isEs ? "Menú de navegación" : "Navigation menu"}
|
||||
aria-label={
|
||||
isPt
|
||||
? "Menu de navegação"
|
||||
: isEs
|
||||
? "Menú de navegación"
|
||||
: isFr
|
||||
? "Menu de navigation"
|
||||
: isDe
|
||||
? "Navigationsmenü"
|
||||
: "Navigation menu"
|
||||
}
|
||||
aria-hidden={!drawerOpen}
|
||||
inert={!drawerOpen}
|
||||
className={`fixed inset-y-0 right-0 z-[101] flex w-[min(23rem,94vw)] flex-col rounded-l-[2rem] border-l border-line bg-canvas transition-transform duration-300 ${
|
||||
@@ -329,15 +471,19 @@ export default function Header() {
|
||||
>
|
||||
<div className="flex items-start justify-between px-5 py-5">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted">{isPt ? "Navegação" : isEs ? "Navegación" : "Navigation"}</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold tracking-[-0.04em] text-ink">{isPt ? "Para onde ir" : isEs ? "A dónde ir" : "Where to go"}</p>
|
||||
<p className="text-xs font-semibold text-muted">
|
||||
{isPt ? "Navegação" : isEs ? "Navegación" : isFr ? "Navigation" : isDe ? "Navigation" : "Navigation"}
|
||||
</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold tracking-[-0.04em] text-ink">
|
||||
{isPt ? "Para onde ir" : isEs ? "A dónde ir" : isFr ? "Où aller" : isDe ? "Wohin möchten Sie" : "Where to go"}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
ref={closeRef}
|
||||
type="button"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="grid h-10 w-10 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
|
||||
aria-label={isPt ? "Fechar menu" : isEs ? "Cerrar menú" : "Close menu"}
|
||||
aria-label={isPt ? "Fechar menu" : isEs ? "Cerrar menú" : isFr ? "Fermer le menu" : isDe ? "Menü schließen" : "Close menu"}
|
||||
>
|
||||
<X className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -369,7 +515,10 @@ export default function Header() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-2" aria-label={isPt ? "Navegação móvel" : isEs ? "Navegación móvil" : "Mobile navigation"}>
|
||||
<nav
|
||||
className="flex-1 overflow-y-auto px-3 py-2"
|
||||
aria-label={isPt ? "Navegação móvel" : isEs ? "Navegación móvil" : isFr ? "Navigation mobile" : isDe ? "Mobile Navigation" : "Mobile navigation"}
|
||||
>
|
||||
{visibleMobileLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
@@ -387,7 +536,7 @@ export default function Header() {
|
||||
<div className="p-4 pt-2">
|
||||
<Link href={getLinkHref("/seletor")} onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full">
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
{isPt ? "Selecionar meu chip" : isEs ? "Seleccionar mi chip" : "Select my chip"}
|
||||
{isPt ? "Selecionar meu chip" : isEs ? "Seleccionar mi chip" : isFr ? "Choisir ma puce" : isDe ? "Meinen Chip auswählen" : "Select my chip"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -398,7 +547,7 @@ export default function Header() {
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeToggle({ isPt = true, isEs = false }) {
|
||||
function ThemeToggle({ isPt = true, isEs = false, isFr = false, isDe = false }) {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -415,8 +564,48 @@ function ThemeToggle({ isPt = true, isEs = false }) {
|
||||
type="button"
|
||||
onClick={() => setTheme(dark ? "light" : "dark")}
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
|
||||
aria-label={dark ? (isPt ? "Ativar tema claro" : isEs ? "Activar tema claro" : "Switch to light theme") : (isPt ? "Ativar tema escuro" : isEs ? "Activar tema oscuro" : "Switch to dark theme")}
|
||||
title={dark ? (isPt ? "Tema claro" : isEs ? "Tema claro" : "Light theme") : (isPt ? "Tema escuro" : isEs ? "Tema oscuro" : "Dark theme")}
|
||||
aria-label={
|
||||
dark
|
||||
? isPt
|
||||
? "Ativar tema claro"
|
||||
: isEs
|
||||
? "Activar tema claro"
|
||||
: isFr
|
||||
? "Activer le thème clair"
|
||||
: isDe
|
||||
? "Helles Design aktivieren"
|
||||
: "Switch to light theme"
|
||||
: isPt
|
||||
? "Ativar tema escuro"
|
||||
: isEs
|
||||
? "Activar tema oscuro"
|
||||
: isFr
|
||||
? "Activer le thème sombre"
|
||||
: isDe
|
||||
? "Dunkles Design aktivieren"
|
||||
: "Switch to dark theme"
|
||||
}
|
||||
title={
|
||||
dark
|
||||
? isPt
|
||||
? "Tema claro"
|
||||
: isEs
|
||||
? "Tema claro"
|
||||
: isFr
|
||||
? "Thème clair"
|
||||
: isDe
|
||||
? "Helles Design"
|
||||
: "Light theme"
|
||||
: isPt
|
||||
? "Tema escuro"
|
||||
: isEs
|
||||
? "Tema oscuro"
|
||||
: isFr
|
||||
? "Thème sombre"
|
||||
: isDe
|
||||
? "Dunkles Design"
|
||||
: "Dark theme"
|
||||
}
|
||||
>
|
||||
{dark ? <Sun className="h-4 w-4" aria-hidden="true" /> : <Moon className="h-4 w-4" aria-hidden="true" />}
|
||||
</button>
|
||||
|
||||
@@ -63,8 +63,26 @@ export default function LanguageSelector() {
|
||||
className="flex h-11 min-w-[44px] items-center gap-1.5 rounded-full border border-line bg-surface px-3 text-sm font-semibold text-ink transition-colors hover:bg-surface-2 focus-visible:outline-2 focus-visible:outline-accent"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
aria-label={currentLocaleCode === "en" ? `Current language: ${currentLocale.label}. Click to switch language.` : currentLocaleCode === "es" ? `Idioma actual: ${currentLocale.label}. Haga clic para cambiar de idioma.` : `Idioma atual: ${currentLocale.label}. Clique para trocar de idioma.`}
|
||||
title={currentLocaleCode === "en" ? `Language: ${currentLocale.label}` : `Idioma: ${currentLocale.label}`}
|
||||
aria-label={
|
||||
currentLocaleCode === "en"
|
||||
? `Current language: ${currentLocale.label}. Click to switch language.`
|
||||
: currentLocaleCode === "es"
|
||||
? `Idioma actual: ${currentLocale.label}. Haga clic para cambiar de idioma.`
|
||||
: currentLocaleCode === "fr"
|
||||
? `Langue actuelle : ${currentLocale.label}. Cliquez pour changer de langue.`
|
||||
: currentLocaleCode === "de"
|
||||
? `Aktuelle Sprache: ${currentLocale.label}. Klicken Sie, um die Sprache zu wechseln.`
|
||||
: `Idioma atual: ${currentLocale.label}. Clique para trocar de idioma.`
|
||||
}
|
||||
title={
|
||||
currentLocaleCode === "en"
|
||||
? `Language: ${currentLocale.label}`
|
||||
: currentLocaleCode === "fr"
|
||||
? `Langue : ${currentLocale.label}`
|
||||
: currentLocaleCode === "de"
|
||||
? `Sprache: ${currentLocale.label}`
|
||||
: `Idioma: ${currentLocale.label}`
|
||||
}
|
||||
>
|
||||
<Globe className="h-4 w-4 text-muted" aria-hidden="true" />
|
||||
<span className="font-semibold">{currentLocale.short}</span>
|
||||
@@ -77,7 +95,17 @@ export default function LanguageSelector() {
|
||||
{open && (
|
||||
<div
|
||||
role="listbox"
|
||||
aria-label={currentLocaleCode === "en" ? "Available languages" : currentLocaleCode === "es" ? "Idiomas disponibles" : "Idiomas disponíveis"}
|
||||
aria-label={
|
||||
currentLocaleCode === "en"
|
||||
? "Available languages"
|
||||
: currentLocaleCode === "es"
|
||||
? "Idiomas disponibles"
|
||||
: currentLocaleCode === "fr"
|
||||
? "Langues disponibles"
|
||||
: currentLocaleCode === "de"
|
||||
? "Verfügbare Sprachen"
|
||||
: "Idiomas disponíveis"
|
||||
}
|
||||
className="absolute right-0 top-full mt-2 w-48 rounded-2xl border border-line bg-surface p-1.5 shadow-[0_24px_60px_-20px_rgba(15,23,42,0.35)] dark:shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] z-50"
|
||||
>
|
||||
{LOCALES.map((loc) => {
|
||||
|
||||
+29
-3
@@ -15,6 +15,8 @@ export default function PageHero({
|
||||
children,
|
||||
isEn: isEnProp,
|
||||
isEs: isEsProp,
|
||||
isFr: isFrProp,
|
||||
isDe: isDeProp,
|
||||
locale: localeProp,
|
||||
}) {
|
||||
const center = align === "center";
|
||||
@@ -30,13 +32,37 @@ export default function PageHero({
|
||||
: localeProp
|
||||
? localeProp === "es"
|
||||
: trail.some((t) => t.isEs || (t.href && t.href.startsWith("/es")));
|
||||
const isFr =
|
||||
isFrProp !== undefined
|
||||
? isFrProp
|
||||
: localeProp
|
||||
? localeProp === "fr"
|
||||
: trail.some((t) => t.isFr || (t.href && t.href.startsWith("/fr")));
|
||||
const isDe =
|
||||
isDeProp !== undefined
|
||||
? isDeProp
|
||||
: localeProp
|
||||
? localeProp === "de"
|
||||
: trail.some((t) => t.isDe || (t.href && t.href.startsWith("/de")));
|
||||
|
||||
const homeHref = isEn ? "/en" : isEs ? "/es" : isFr ? "/fr" : isDe ? "/de" : "/";
|
||||
const homeLabel = isEn ? "Home" : isEs ? "Inicio" : isFr ? "Accueil" : isDe ? "Startseite" : "Início";
|
||||
const breadcrumbAria = isEn
|
||||
? "Breadcrumb navigation"
|
||||
: isEs
|
||||
? "Navegación por migas de pan"
|
||||
: isFr
|
||||
? "Fil d'Ariane"
|
||||
: isDe
|
||||
? "Brotkrumennavigation"
|
||||
: "Trilha de navegação";
|
||||
|
||||
return (
|
||||
<section className="shell pt-8 sm:pt-12">
|
||||
{trail.length > 0 && (
|
||||
<nav aria-label={isEn ? "Breadcrumb navigation" : isEs ? "Navegación por migas de pan" : "Trilha de navegação"} className="flex flex-wrap items-center gap-2 text-sm text-muted">
|
||||
<Link href={isEn ? "/en" : isEs ? "/es" : "/"} className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
|
||||
{isEn ? "Home" : isEs ? "Inicio" : "Início"}
|
||||
<nav aria-label={breadcrumbAria} className="flex flex-wrap items-center gap-2 text-sm text-muted">
|
||||
<Link href={homeHref} className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
|
||||
{homeLabel}
|
||||
</Link>
|
||||
{trail.map((item, i) => (
|
||||
<span key={item.label} className="flex min-h-11 items-center gap-2">
|
||||
|
||||
@@ -201,14 +201,144 @@ const GRUPOS_ES = [
|
||||
},
|
||||
];
|
||||
|
||||
const GRUPOS_FR = [
|
||||
{
|
||||
id: "entrada",
|
||||
titulo: "Entrée uniquement",
|
||||
icone: ArrowDownToLine,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Broches en entrée seule. Elles ne peuvent pas être configurées en sortie et ne possèdent pas de résistances de pull-up ou de pull-down internes. Si vous en avez besoin, ajoutez-en dans votre circuit externe.",
|
||||
combina: (avisos) => /entrée uniquement|input[- ]only/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "flash",
|
||||
titulo: "Liées à la mémoire flash/PSRAM",
|
||||
icone: HardDrive,
|
||||
cor: "text-copper",
|
||||
explicacao:
|
||||
"Déjà connectées à la mémoire depuis laquelle le firmware est exécuté. Les utiliser comme GPIO standard plante généralement la puce ou corrompt l'exécution. Sur les modules avec flash interne, elles ne sont même pas reliées aux broches du boîtier.",
|
||||
combina: (avisos) =>
|
||||
/(flash|psram)/i.test(avisos) &&
|
||||
/(dédi|mémoire|ne doit pas|déconseillé|connected to|dedicated to|should not)/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "strapping",
|
||||
titulo: "Strapping (mode de démarrage)",
|
||||
icone: Power,
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"La puce lit l'état logique de ces broches au moment du reset pour déterminer son mode de démarrage. Vous pouvez les utiliser après le boot, mais le circuit externe ne doit pas forcer un niveau incorrect pendant le reset, sinon la carte ne démarrera pas ou entrera en mode flashage de manière inattendue.",
|
||||
combina: (avisos, cats) =>
|
||||
cats.includes("Strapping") ||
|
||||
/strapping|mode de démarrage|démarrage|boot mode/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "debug",
|
||||
titulo: "Programmation et débogage",
|
||||
icone: Bug,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Ce sont l'UART de programmation et les lignes JTAG. Elles restent utilisables comme GPIO, mais les réutiliser supprime la possibilité de flasher et de déboguer la carte sans déconnecter ce qui y est relié.",
|
||||
combina: (avisos, cats) => /débogage|programmation|debugging/i.test(avisos) || cats.includes("JTAG"),
|
||||
},
|
||||
{
|
||||
id: "usb",
|
||||
titulo: "USB natif",
|
||||
icone: Usb,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Reliées par défaut au contrôleur USB Série/JTAG interne. Les utiliser comme GPIO nécessite de désactiver ce périphérique, ce qui désactive également le flashage direct par USB.",
|
||||
combina: (avisos) => /usb (série|serial)\/jtag|usb_d[+-]/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "eletrico",
|
||||
titulo: "Ne doivent pas être laissées flottantes",
|
||||
icone: Zap,
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"Nécessitent un niveau logique matériel défini : pull-up externe ou circuit RC de temporisation. Les laisser flottantes entraîne des redémarrages aléatoires ou un démarrage instable.",
|
||||
combina: (avisos) => /flottant|floating/i.test(avisos),
|
||||
},
|
||||
];
|
||||
|
||||
const GRUPOS_DE = [
|
||||
{
|
||||
id: "entrada",
|
||||
titulo: "Nur Eingang",
|
||||
icone: ArrowDownToLine,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Pins, die nur als Eingang arbeiten. Sie können nicht als Ausgang konfiguriert werden und besitzen keine internen Pull-up- oder Pull-down-Widerstände. Falls erforderlich, sehen Sie diese in Ihrer externen Schaltung vor.",
|
||||
combina: (avisos) => /nur eingang|input[- ]only/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "flash",
|
||||
titulo: "Mit Flash-/PSRAM-Speicher verbunden",
|
||||
icone: HardDrive,
|
||||
cor: "text-copper",
|
||||
explicacao:
|
||||
"Bereits mit dem Speicher verbunden, aus dem die Firmware ausgeführt wird. Die Verwendung als universelle GPIOs führt meist zu Abstürzen oder Speicherbeschädigungen. Bei Modulen mit internem Flash sind sie nicht nach außen geführt.",
|
||||
combina: (avisos) =>
|
||||
/(flash|psram)/i.test(avisos) &&
|
||||
/(dediziert|speicher|sollte nicht|nicht empfohlen|connected to|dedicated to|should not)/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "strapping",
|
||||
titulo: "Strapping-Pins (Bootmodus)",
|
||||
icone: Power,
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"Der Chip liest den Logikpegel dieser Pins beim Reset aus, um den Bootmodus festzulegen. Nach dem Booten können sie genutzt werden, die externe Schaltung darf beim Reset jedoch keinen falschen Pegel erzwingen, da das Board sonst nicht startet oder unerwartet in den Download-Modus wechselt.",
|
||||
combina: (avisos, cats) =>
|
||||
cats.includes("Strapping") ||
|
||||
/strapping|bootmodus|boot-modus|boot mode/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "debug",
|
||||
titulo: "Programmierung und Debugging",
|
||||
icone: Bug,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Dies sind die Flash-UART- und JTAG-Leitungen. Sie können als GPIO genutzt werden, doch dadurch entfällt die Möglichkeit, das Board ohne Trennen der Peripherie zu programmieren oder zu debuggen.",
|
||||
combina: (avisos, cats) => /debugging|programmierung/i.test(avisos) || cats.includes("JTAG"),
|
||||
},
|
||||
{
|
||||
id: "usb",
|
||||
titulo: "Natives USB",
|
||||
icone: Usb,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Standardmäßig mit dem internen USB-Serial/JTAG-Controller verbunden. Um sie als GPIO zu nutzen, muss diese Funktion deaktiviert werden, wodurch auch das direkte Flashen über USB entfällt.",
|
||||
combina: (avisos) => /usb[- ](serial|seriell)\/jtag|usb_d[+-]/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "eletrico",
|
||||
titulo: "Dürfen nicht unbeschaltet bleiben",
|
||||
icone: Zap,
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"Erfordern einen definierten Hardwarepegel: externen Pull-up oder RC-Verzögerungsschaltung. Bleiben sie unbeschaltet (floating), führt dies zu sporadischen Resets oder instabilem Bootverhalten.",
|
||||
combina: (avisos) => /unbeschaltet|floating/i.test(avisos),
|
||||
},
|
||||
];
|
||||
|
||||
/** Nome do pino + o GPIO correspondente, quando o nome é uma função (ex.: SENSOR_VP). */
|
||||
function rotuloDoPino(pino) {
|
||||
const gpio = (pino.funcoes || []).find((f) => /^GPIO\d+$/.test(f));
|
||||
return gpio && gpio !== pino.nome ? `${pino.nome} · ${gpio}` : pino.nome;
|
||||
}
|
||||
|
||||
function derivarGrupos(conexoes, isEn, isEs) {
|
||||
const listaGrupos = isEn ? GRUPOS_EN : isEs ? GRUPOS_ES : GRUPOS_PT;
|
||||
function derivarGrupos(conexoes, isEn, isEs, isFr, isDe) {
|
||||
const listaGrupos = isEn
|
||||
? GRUPOS_EN
|
||||
: isEs
|
||||
? GRUPOS_ES
|
||||
: isFr
|
||||
? GRUPOS_FR
|
||||
: isDe
|
||||
? GRUPOS_DE
|
||||
: GRUPOS_PT;
|
||||
return listaGrupos.map((grupo) => {
|
||||
const vistos = new Map();
|
||||
for (const pino of conexoes) {
|
||||
@@ -224,13 +354,15 @@ function derivarGrupos(conexoes, isEn, isEs) {
|
||||
}).filter((g) => g.pinos.length > 0);
|
||||
}
|
||||
|
||||
export default function PinRestrictions({ conexoes, serieKey, isEn = false, isEs = false, locale }) {
|
||||
export default function PinRestrictions({ conexoes, serieKey, isEn = false, isEs = false, isFr = false, isDe = false, locale }) {
|
||||
if (!conexoes?.length) return null;
|
||||
|
||||
const effectiveIsEn = isEn || locale === "en";
|
||||
const effectiveIsEs = isEs || locale === "es";
|
||||
const effectiveIsFr = isFr || locale === "fr";
|
||||
const effectiveIsDe = isDe || locale === "de";
|
||||
|
||||
const grupos = derivarGrupos(conexoes, effectiveIsEn, effectiveIsEs);
|
||||
const grupos = derivarGrupos(conexoes, effectiveIsEn, effectiveIsEs, effectiveIsFr, effectiveIsDe);
|
||||
if (!grupos.length) return null;
|
||||
|
||||
const totalPinos = new Set(grupos.flatMap((g) => g.pinos)).size;
|
||||
@@ -241,7 +373,15 @@ export default function PinRestrictions({ conexoes, serieKey, isEn = false, isEs
|
||||
<span className="grid h-9 w-9 place-items-center rounded-full bg-gold-soft text-gold">
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
{effectiveIsEn ? "Pins with usage restrictions" : effectiveIsEs ? "Pines con restricción de uso" : "Pinos com restrição de uso"}
|
||||
{effectiveIsEn
|
||||
? "Pins with usage restrictions"
|
||||
: effectiveIsEs
|
||||
? "Pines con restricción de uso"
|
||||
: effectiveIsFr
|
||||
? "Broches avec restrictions d'utilisation"
|
||||
: effectiveIsDe
|
||||
? "Pins mit Nutzungseinschränkungen"
|
||||
: "Pinos com restrição de uso"}
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
|
||||
@@ -253,6 +393,14 @@ export default function PinRestrictions({ conexoes, serieKey, isEn = false, isEs
|
||||
<>
|
||||
No todos los pines del {serieKey} están libres para uso general. {totalPinos} de ellos tienen alguna condición: ya están ocupados por memoria, se leen durante el arranque o solo funcionan como entrada. Conviene comprobarlos antes de cerrar el esquemático.
|
||||
</>
|
||||
) : effectiveIsFr ? (
|
||||
<>
|
||||
Toutes les broches de l'{serieKey} ne sont pas libres pour un usage général. {totalPinos} d'entre elles ont des conditions : occupées par la mémoire, lues pendant le démarrage ou fonctionnent uniquement en entrée. Pensez à vérifier avant de finaliser votre schéma.
|
||||
</>
|
||||
) : effectiveIsDe ? (
|
||||
<>
|
||||
Nicht jeder Pin des {serieKey} steht für allgemeine Zwecke frei zur Verfügung. {totalPinos} davon unterliegen Bedingungen: durch Speicher belegt, beim Booten abgefragt oder reine Eingangspins. Prüfen Sie dies vor Abschluss Ihres Schaltplans.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Nem todo pino do {serieKey} está livre para uso geral. {totalPinos} deles têm alguma
|
||||
@@ -269,7 +417,26 @@ export default function PinRestrictions({ conexoes, serieKey, isEn = false, isEs
|
||||
<Icone className={`h-4 w-4 shrink-0 ${corIcone}`} aria-hidden="true" />
|
||||
<h3 className="text-sm font-semibold text-ink">{titulo}</h3>
|
||||
<span className="ml-auto text-xs text-muted tabular-nums">
|
||||
{pinos.length} {pinos.length === 1 ? (effectiveIsEn ? "pin" : effectiveIsEs ? "pin" : "pino") : (effectiveIsEn ? "pins" : effectiveIsEs ? "pines" : "pinos")}
|
||||
{pinos.length}{" "}
|
||||
{pinos.length === 1
|
||||
? effectiveIsEn
|
||||
? "pin"
|
||||
: effectiveIsEs
|
||||
? "pin"
|
||||
: effectiveIsFr
|
||||
? "broche"
|
||||
: effectiveIsDe
|
||||
? "Pin"
|
||||
: "pino"
|
||||
: effectiveIsEn
|
||||
? "pins"
|
||||
: effectiveIsEs
|
||||
? "pines"
|
||||
: effectiveIsFr
|
||||
? "broches"
|
||||
: effectiveIsDe
|
||||
? "Pins"
|
||||
: "pinos"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -292,12 +459,16 @@ export default function PinRestrictions({ conexoes, serieKey, isEn = false, isEs
|
||||
"These groups are derived from the same data as the pinout diagram. There you can see every pin's full functions, power domain, and complete engineering notes."
|
||||
) : effectiveIsEs ? (
|
||||
"Estos grupos proceden de los mismos datos que el diagrama de conexiones. Allí podrá consultar pino a pino todas las funciones, el dominio de alimentación y los avisos completos."
|
||||
) : effectiveIsFr ? (
|
||||
"Ces groupes sont issus des mêmes données que le schéma de brochage. Vous pouvez y consulter broche par broche l'ensemble des fonctions, le domaine d'alimentation et les remarques complètes."
|
||||
) : effectiveIsDe ? (
|
||||
"Diese Gruppen stammen aus denselben Daten wie das Pinbelegungsdiagramm. Dort finden Sie für jeden Pin alle Funktionen, den Spannungsbereich und die vollständigen Entwicklungshinweise."
|
||||
) : (
|
||||
"Estes grupos vêm dos mesmos dados do diagrama de conexões. Lá você vê pino a pino todas as funções, o domínio de alimentação e os avisos completos."
|
||||
)}
|
||||
</p>
|
||||
<Link href="#connections" className="btn btn-ghost shrink-0">
|
||||
{effectiveIsEn ? "Open pinout diagram" : effectiveIsEs ? "Abrir diagrama de pines" : "Abrir diagrama de pinos"}
|
||||
{effectiveIsEn ? "Open pinout diagram" : effectiveIsEs ? "Abrir diagrama de pines" : effectiveIsFr ? "Ouvrir le brochage" : effectiveIsDe ? "Pinbelegung öffnen" : "Abrir diagrama de pinos"}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+141
-25
@@ -54,10 +54,34 @@ const TIPOS_ES = {
|
||||
componente: { label: "Componente", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
|
||||
};
|
||||
|
||||
const TIPOS_FR = {
|
||||
resposta: { label: "Réponse", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
|
||||
pino: { label: "Broches", Icon: Plug, cor: "bg-accent text-accent-contrast" },
|
||||
pagina: { label: "Page", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
|
||||
serie: { label: "Série", Icon: Cpu, cor: "bg-accent-soft text-accent" },
|
||||
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
|
||||
glossario: { label: "Glossaire", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
|
||||
erro: { label: "Erreur", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
|
||||
componente: { label: "Composant", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
|
||||
};
|
||||
|
||||
const TIPOS_DE = {
|
||||
resposta: { label: "Antwort", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
|
||||
pino: { label: "Pins", Icon: Plug, cor: "bg-accent text-accent-contrast" },
|
||||
pagina: { label: "Seite", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
|
||||
serie: { label: "Serie", Icon: Cpu, cor: "bg-accent-soft text-accent" },
|
||||
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
|
||||
glossario: { label: "Glossar", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
|
||||
erro: { label: "Fehler", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
|
||||
componente: { label: "Komponente", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
|
||||
};
|
||||
|
||||
const INTELIGENTES = new Set(["resposta", "pino"]);
|
||||
|
||||
const EXEMPLOS_EN = ["flash s3", "usb pins c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
||||
const EXEMPLOS_ES = ["flash s3", "pines usb c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
||||
const EXEMPLOS_FR = ["flash s3", "broches usb c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
||||
const EXEMPLOS_DE = ["flash s3", "usb pins c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
||||
|
||||
function Destaque({ texto, termos }) {
|
||||
if (!termos.length) return texto;
|
||||
@@ -110,12 +134,22 @@ function Pilula({ children, onClick, forte = false }) {
|
||||
|
||||
export default function SearchDialog() {
|
||||
const pathname = usePathname() || "";
|
||||
const locale = pathname === "/en" || pathname.startsWith("/en/") ? "en" : (pathname === "/es" || pathname.startsWith("/es/") ? "es" : "pt");
|
||||
const locale = pathname === "/en" || pathname.startsWith("/en/")
|
||||
? "en"
|
||||
: pathname === "/es" || pathname.startsWith("/es/")
|
||||
? "es"
|
||||
: pathname === "/fr" || pathname.startsWith("/fr/")
|
||||
? "fr"
|
||||
: pathname === "/de" || pathname.startsWith("/de/")
|
||||
? "de"
|
||||
: "pt";
|
||||
const isEn = locale === "en";
|
||||
const isEs = locale === "es";
|
||||
const isFr = locale === "fr";
|
||||
const isDe = locale === "de";
|
||||
|
||||
const TIPOS = isEn ? TIPOS_EN : isEs ? TIPOS_ES : TIPOS_PT;
|
||||
const EXEMPLOS = isEn ? EXEMPLOS_EN : isEs ? EXEMPLOS_ES : EXEMPLOS_DE_BUSCA;
|
||||
const TIPOS = isEn ? TIPOS_EN : isEs ? TIPOS_ES : isFr ? TIPOS_FR : isDe ? TIPOS_DE : TIPOS_PT;
|
||||
const EXEMPLOS = isEn ? EXEMPLOS_EN : isEs ? EXEMPLOS_ES : isFr ? EXEMPLOS_FR : isDe ? EXEMPLOS_DE : EXEMPLOS_DE_BUSCA;
|
||||
|
||||
const [aberto, setAberto] = useState(false);
|
||||
const [montado, setMontado] = useState(false);
|
||||
@@ -156,7 +190,15 @@ export default function SearchDialog() {
|
||||
const carregarIndice = useCallback(() => {
|
||||
setErro(false);
|
||||
let cancelado = false;
|
||||
const url = isEn ? "/api/busca/en?v=2" : isEs ? "/api/busca/es?v=2" : "/api/busca?v=2";
|
||||
const url = isEn
|
||||
? "/api/busca/en?v=2"
|
||||
: isEs
|
||||
? "/api/busca/es?v=2"
|
||||
: isFr
|
||||
? "/api/busca/fr?v=2"
|
||||
: isDe
|
||||
? "/api/busca/de?v=2"
|
||||
: "/api/busca?v=2";
|
||||
fetch(url, { cache: "no-cache" })
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
@@ -180,7 +222,7 @@ export default function SearchDialog() {
|
||||
return () => {
|
||||
cancelado = true;
|
||||
};
|
||||
}, [isEn, isEs]);
|
||||
}, [isEn, isEs, isFr, isDe]);
|
||||
|
||||
// Carrega o índice (textos + especificações + pinos) na primeira abertura.
|
||||
useEffect(() => {
|
||||
@@ -251,17 +293,17 @@ export default function SearchDialog() {
|
||||
id="botao-busca"
|
||||
onClick={() => setAberto(true)}
|
||||
className="flex h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full border border-line px-3 text-ink transition-colors hover:bg-surface-2 xl:px-4"
|
||||
aria-label={isEn ? "Search ESPDocs (Ctrl+K)" : isEs ? "Buscar en ESPDocs (Ctrl+K)" : "Buscar no ESPDocs (Ctrl+K)"}
|
||||
aria-label={isEn ? "Search ESPDocs (Ctrl+K)" : isEs ? "Buscar en ESPDocs (Ctrl+K)" : isFr ? "Rechercher sur ESPDocs (Ctrl+K)" : isDe ? "ESPDocs durchsuchen (Strg+K)" : "Buscar no ESPDocs (Ctrl+K)"}
|
||||
>
|
||||
<Search className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="hidden text-sm font-medium xl:inline">{isEn ? "Search" : "Buscar"}</span>
|
||||
<kbd className="hidden rounded-md border border-line bg-surface-2 px-1.5 py-0.5 font-mono text-xs font-semibold text-muted xl:inline">Ctrl K</kbd>
|
||||
<span className="hidden text-sm font-medium xl:inline">{isEn ? "Search" : isEs ? "Buscar" : isFr ? "Rechercher" : isDe ? "Suchen" : "Buscar"}</span>
|
||||
<kbd className="hidden rounded-md border border-line bg-surface-2 px-1.5 py-0.5 font-mono text-xs font-semibold text-muted xl:inline">{isDe ? "Strg K" : "Ctrl K"}</kbd>
|
||||
</button>
|
||||
|
||||
{montado &&
|
||||
aberto &&
|
||||
createPortal(
|
||||
<div className="fixed inset-0 z-[120] flex items-start justify-center px-4 pt-[8vh]" role="dialog" aria-modal="true" aria-label={isEn ? "Search ESPDocs" : isEs ? "Buscar en ESPDocs" : "Buscar no ESPDocs"}>
|
||||
<div className="fixed inset-0 z-[120] flex items-start justify-center px-4 pt-[8vh]" role="dialog" aria-modal="true" aria-label={isEn ? "Search ESPDocs" : isEs ? "Buscar en ESPDocs" : isFr ? "Rechercher sur ESPDocs" : isDe ? "ESPDocs durchsuchen" : "Buscar no ESPDocs"}>
|
||||
<div className="dialog-scrim absolute inset-0 bg-black/45 backdrop-blur-sm" onClick={fechar} />
|
||||
<div className="dialog-panel relative flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden rounded-3xl border border-line bg-canvas shadow-2xl">
|
||||
<div className="flex items-center gap-3 border-b border-line px-5">
|
||||
@@ -273,17 +315,27 @@ export default function SearchDialog() {
|
||||
value={consulta}
|
||||
onChange={(e) => setConsulta(e.target.value)}
|
||||
onKeyDown={onInputKey}
|
||||
placeholder={isEn ? "Ask ESPDocs: “flash s3”, “usb pins c6”, “gpio0 esp32”…" : isEs ? "Pregunte a ESPDocs: “flash s3”, “pines usb c6”, “gpio0 esp32”…" : "Pergunte ao ESPDocs: “flash s3”, “pinos usb c6”, “gpio0 esp32”…"}
|
||||
placeholder={
|
||||
isEn
|
||||
? "Ask ESPDocs: “flash s3”, “usb pins c6”, “gpio0 esp32”…"
|
||||
: isEs
|
||||
? "Pregunte a ESPDocs: “flash s3”, “pines usb c6”, “gpio0 esp32”…"
|
||||
: isFr
|
||||
? "Demandez à ESPDocs : « flash s3 », « broches usb c6 », « gpio0 esp32 »…"
|
||||
: isDe
|
||||
? "ESPDocs fragen: „flash s3“, „usb pins c6“, „gpio0 esp32“… "
|
||||
: "Pergunte ao ESPDocs: “flash s3”, “pinos usb c6”, “gpio0 esp32”…"
|
||||
}
|
||||
className="flex-1 bg-transparent py-5 text-base text-ink outline-none placeholder:text-muted"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
aria-label={isEn ? "Search query" : isEs ? "Término de búsqueda" : "Termo de busca"}
|
||||
aria-label={isEn ? "Search query" : isEs ? "Término de búsqueda" : isFr ? "Requête de recherche" : isDe ? "Suchbegriff" : "Termo de busca"}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fechar}
|
||||
className="grid h-9 w-9 cursor-pointer place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink"
|
||||
aria-label={isEn ? "Close search" : isEs ? "Cerrar búsqueda" : "Fechar busca"}
|
||||
aria-label={isEn ? "Close search" : isEs ? "Cerrar búsqueda" : isFr ? "Fermer la recherche" : isDe ? "Suche schließen" : "Fechar busca"}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -291,7 +343,7 @@ export default function SearchDialog() {
|
||||
|
||||
{entendeu && (
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-line bg-surface px-5 py-2.5">
|
||||
<span className="text-xs font-semibold text-muted">{isEn ? "Understood:" : isEs ? "Entendido:" : "Entendi:"}</span>
|
||||
<span className="text-xs font-semibold text-muted">{isEn ? "Understood:" : isEs ? "Entendido:" : isFr ? "Compris :" : isDe ? "Erkannt:" : "Entendi:"}</span>
|
||||
{interpretacao.chips.map((c) => (
|
||||
<Pilula key={c.chave} forte>{c.nome}</Pilula>
|
||||
))}
|
||||
@@ -303,7 +355,11 @@ export default function SearchDialog() {
|
||||
|
||||
{sugestoes.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-line px-5 py-2.5">
|
||||
<span className="text-xs font-semibold text-muted">{interpretacao.chips.length ? (isEn ? "Ask about:" : isEs ? "Preguntar sobre:" : "Perguntar sobre:") : (isEn ? "Which chip?" : isEs ? "¿Para qué chip?" : "Para qual chip?")}</span>
|
||||
<span className="text-xs font-semibold text-muted">
|
||||
{interpretacao.chips.length
|
||||
? (isEn ? "Ask about:" : isEs ? "Preguntar sobre:" : isFr ? "Poser une question sur :" : isDe ? "Frage zu:" : "Perguntar sobre:")
|
||||
: (isEn ? "Which chip?" : isEs ? "¿Para qué chip?" : isFr ? "Pour quelle puce ?" : isDe ? "Für welchen Chip?" : "Para qual chip?")}
|
||||
</span>
|
||||
{sugestoes.map((s) => (
|
||||
<Pilula key={s.rotulo} onClick={() => anexar(s.anexar)}>
|
||||
{s.rotulo}
|
||||
@@ -315,21 +371,37 @@ export default function SearchDialog() {
|
||||
<div ref={listaRef} className="flex-1 overflow-y-auto p-2" role="listbox">
|
||||
{erro && (
|
||||
<div className="p-6 text-center">
|
||||
<p className="text-sm font-semibold text-danger">{isEn ? "Could not load search index." : isEs ? "No se pudo cargar el índice de búsqueda." : "Não foi possível carregar o índice de busca."}</p>
|
||||
<p className="text-sm font-semibold text-danger">
|
||||
{isEn
|
||||
? "Could not load search index."
|
||||
: isEs
|
||||
? "No se pudo cargar el índice de búsqueda."
|
||||
: isFr
|
||||
? "Impossible de charger l'index de recherche."
|
||||
: isDe
|
||||
? "Suchindex konnte nicht geladen werden."
|
||||
: "Não foi possível carregar o índice de busca."}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={carregarIndice}
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
>
|
||||
{isEn ? "Try again" : isEs ? "Reintentar" : "Tentar novamente"}
|
||||
{isEn ? "Try again" : isEs ? "Reintentar" : isFr ? "Réessayer" : isDe ? "Erneut versuchen" : "Tentar novamente"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!erro && !indice && <p className="p-6 text-center text-sm text-muted">{isEn ? "Loading…" : isEs ? "Cargando…" : "Carregando…"}</p>}
|
||||
{!erro && !indice && (
|
||||
<p className="p-6 text-center text-sm text-muted">
|
||||
{isEn ? "Loading…" : isEs ? "Cargando…" : isFr ? "Chargement…" : isDe ? "Wird geladen…" : "Carregando…"}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{indice && vazia && (
|
||||
<div className="px-3 pb-3 pt-1">
|
||||
<p className="text-xs font-semibold text-muted">{isEn ? "Try asking" : isEs ? "Pruebe a preguntar" : "Experimente perguntar"}</p>
|
||||
<p className="text-xs font-semibold text-muted">
|
||||
{isEn ? "Try asking" : isEs ? "Pruebe a preguntar" : isFr ? "Essayez de demander" : isDe ? "Versuchen Sie" : "Experimente perguntar"}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{EXEMPLOS.map((ex) => (
|
||||
<Pilula key={ex} onClick={() => setConsulta(ex)}>
|
||||
@@ -337,14 +409,36 @@ export default function SearchDialog() {
|
||||
</Pilula>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-xs font-semibold text-muted">{isEn ? "Go to" : isEs ? "Ir a" : "Ir para"}</p>
|
||||
<p className="mt-4 text-xs font-semibold text-muted">
|
||||
{isEn ? "Go to" : isEs ? "Ir a" : isFr ? "Aller à" : isDe ? "Gehe zu" : "Ir para"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{indice && !vazia && resultados.length === 0 && (
|
||||
<div className="p-8 text-center">
|
||||
<p className="font-display text-lg font-semibold text-ink">{isEn ? `No results for “${consulta}”` : isEs ? `Sin resultados para “${consulta}”` : `Nenhum resultado para “${consulta}”`}</p>
|
||||
<p className="mt-1 text-sm text-muted">{isEn ? "Try “brownout”, “flash s3”, “usb pins c6” or “LVGL”." : isEs ? "Pruebe “brownout”, “flash s3”, “pines usb c6” o “LVGL”." : "Tente “brownout”, “flash s3”, “pinos usb c6” ou “LVGL”."}</p>
|
||||
<p className="font-display text-lg font-semibold text-ink">
|
||||
{isEn
|
||||
? `No results for “${consulta}”`
|
||||
: isEs
|
||||
? `Sin resultados para “${consulta}”`
|
||||
: isFr
|
||||
? `Aucun résultat pour « ${consulta} »`
|
||||
: isDe
|
||||
? `Keine Ergebnisse für „${consulta}“`
|
||||
: `Nenhum resultado para “${consulta}”`}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
{isEn
|
||||
? "Try “brownout”, “flash s3”, “usb pins c6” or “LVGL”."
|
||||
: isEs
|
||||
? "Pruebe “brownout”, “flash s3”, “pines usb c6” o “LVGL”."
|
||||
: isFr
|
||||
? "Essayez « brownout », « flash s3 », « broches usb c6 » ou « LVGL »."
|
||||
: isDe
|
||||
? "Versuchen Sie „brownout“, „flash s3“, „usb pins c6“ oder „LVGL“."
|
||||
: "Tente “brownout”, “flash s3”, “pinos usb c6” ou “LVGL”."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -407,10 +501,32 @@ export default function SearchDialog() {
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-4 border-t border-line bg-surface px-4 py-2.5 text-xs text-muted md:flex">
|
||||
<span className="flex items-center gap-1"><ArrowUp className="h-3 w-3" /><ArrowDown className="h-3 w-3" /> {isEn ? "navigate" : "navegar"}</span>
|
||||
<span className="flex items-center gap-1"><CornerDownLeft className="h-3 w-3" /> {isEn ? "open" : "abrir"}</span>
|
||||
<span><kbd className="font-mono font-semibold">Esc</kbd> {isEn ? "close" : isEs ? "cerrar" : "fechar"}</span>
|
||||
<span className="ml-auto">{vazia ? (isEn ? "Tip: press / on any page" : isEs ? "Consejo: presione / en cualquier página" : "Dica: pressione / em qualquer página") : `${resultados.length} ${isEn ? "result(s)" : "resultado(s)"}`}</span>
|
||||
<span className="flex items-center gap-1"><ArrowUp className="h-3 w-3" /><ArrowDown className="h-3 w-3" /> {isEn ? "navigate" : isEs ? "navegar" : isFr ? "naviguer" : isDe ? "navigieren" : "navegar"}</span>
|
||||
<span className="flex items-center gap-1"><CornerDownLeft className="h-3 w-3" /> {isEn ? "open" : isEs ? "abrir" : isFr ? "ouvrir" : isDe ? "öffnen" : "abrir"}</span>
|
||||
<span><kbd className="font-mono font-semibold">Esc</kbd> {isEn ? "close" : isEs ? "cerrar" : isFr ? "fermer" : isDe ? "schließen" : "fechar"}</span>
|
||||
<span className="ml-auto">
|
||||
{vazia
|
||||
? (isEn
|
||||
? "Tip: press / on any page"
|
||||
: isEs
|
||||
? "Consejo: presione / en cualquier página"
|
||||
: isFr
|
||||
? "Astuce : appuyez sur / sur n'importe quelle page"
|
||||
: isDe
|
||||
? "Tipp: Drücken Sie / auf einer beliebigen Seite"
|
||||
: "Dica: pressione / em qualquer página")
|
||||
: `${resultados.length} ${
|
||||
isEn
|
||||
? "result(s)"
|
||||
: isEs
|
||||
? "resultado(s)"
|
||||
: isFr
|
||||
? "résultat(s)"
|
||||
: isDe
|
||||
? "Ergebnis(se)"
|
||||
: "resultado(s)"
|
||||
}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function SeriesTabMenu({ tabs, isEn = false, isEs = false, locale }) {
|
||||
export default function SeriesTabMenu({ tabs, isEn = false, isEs = false, isFr = false, isDe = false, locale }) {
|
||||
const effectiveIsEn = isEn || locale === "en";
|
||||
const effectiveIsEs = isEs || locale === "es";
|
||||
const effectiveIsFr = isFr || locale === "fr";
|
||||
const effectiveIsDe = isDe || locale === "de";
|
||||
const availableTabs = tabs.filter((tab) => tab.available);
|
||||
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id);
|
||||
const refs = useRef({});
|
||||
@@ -61,7 +63,21 @@ export default function SeriesTabMenu({ tabs, isEn = false, isEs = false, locale
|
||||
|
||||
return (
|
||||
<div id="diagrama-abas" className="scroll-mt-28">
|
||||
<div role="tablist" aria-label={effectiveIsEn ? "Board and diagram" : effectiveIsEs ? "Placa y diagrama" : "Placa e diagrama"} className="no-scrollbar flex gap-2 overflow-x-auto pb-1">
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={
|
||||
effectiveIsEn
|
||||
? "Board and diagram"
|
||||
: effectiveIsEs
|
||||
? "Placa y diagrama"
|
||||
: effectiveIsFr
|
||||
? "Carte et schéma"
|
||||
: effectiveIsDe
|
||||
? "Board und Diagramm"
|
||||
: "Placa e diagrama"
|
||||
}
|
||||
className="no-scrollbar flex gap-2 overflow-x-auto pb-1"
|
||||
>
|
||||
{availableTabs.map((tab, indice) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
|
||||
Reference in new issue
Block a user