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
+106
-27
@@ -9,6 +9,8 @@ import Link from "next/link";
|
||||
import seriesDataPt from "@/public/series.json";
|
||||
import seriesDataEn from "@/data/en/series.json";
|
||||
import seriesDataEs from "@/data/es/series.json";
|
||||
import seriesDataFr from "@/data/fr/series.json";
|
||||
import seriesDataDe from "@/data/de/series.json";
|
||||
import { getLocalizedHref } from "@/i18n/links";
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -45,31 +47,51 @@ const FILTROS_ES = [
|
||||
{ id: "ia", rotulo: "Aceleración de IA", testa: (s) => Boolean(s.aceleradores_ia) },
|
||||
];
|
||||
|
||||
const FILTROS_FR = [
|
||||
{ id: "todas", rotulo: "Toutes", testa: () => true },
|
||||
{ id: "riscv", rotulo: "RISC-V", testa: (s) => s.arquitetura.includes("RISC-V") },
|
||||
{ id: "xtensa", rotulo: "Xtensa", testa: (s) => !s.arquitetura.includes("RISC-V") },
|
||||
{ id: "matter", rotulo: "Matter", testa: (s) => s.matter === "Oui" || s.matter === "Yes" || s.matter === "Sim" || s.matter === "Sí" },
|
||||
{ id: "ia", rotulo: "Accélération IA", testa: (s) => Boolean(s.aceleradores_ia) },
|
||||
];
|
||||
|
||||
const FILTROS_DE = [
|
||||
{ id: "todas", rotulo: "Alle", testa: () => true },
|
||||
{ id: "riscv", rotulo: "RISC-V", testa: (s) => s.arquitetura.includes("RISC-V") },
|
||||
{ id: "xtensa", rotulo: "Xtensa", testa: (s) => !s.arquitetura.includes("RISC-V") },
|
||||
{ id: "matter", rotulo: "Matter", testa: (s) => s.matter === "Ja" || s.matter === "Yes" || s.matter === "Sim" || s.matter === "Sí" },
|
||||
{ id: "ia", rotulo: "KI-Beschleunigung", testa: (s) => Boolean(s.aceleradores_ia) },
|
||||
];
|
||||
|
||||
/** Marcador de segurança: o recurso que diferencia a série, não o que todas têm. */
|
||||
function nivelSeguranca(serie, isEn, isEs) {
|
||||
function nivelSeguranca(serie, isEn, isEs, isFr, isDe) {
|
||||
const seg = serie.seguranca || {};
|
||||
if (seg.key_manager === "Sim" || seg.key_manager === "Yes" || seg.key_manager === "Sí") return "Key Manager";
|
||||
if (String(seg.ecdsa).startsWith("Sim") || String(seg.ecdsa).startsWith("Yes") || String(seg.ecdsa).startsWith("Sí")) {
|
||||
return isEs ? "ECDSA en hardware" : isEn ? "Hardware ECDSA" : "ECDSA em hardware";
|
||||
if (seg.key_manager === "Sim" || seg.key_manager === "Yes" || seg.key_manager === "Sí" || seg.key_manager === "Oui" || seg.key_manager === "Ja") return "Key Manager";
|
||||
if (String(seg.ecdsa).startsWith("Sim") || String(seg.ecdsa).startsWith("Yes") || String(seg.ecdsa).startsWith("Sí") || String(seg.ecdsa).startsWith("Oui") || String(seg.ecdsa).startsWith("Ja")) {
|
||||
return isDe ? "Hardware-ECDSA" : isFr ? "ECDSA matériel" : isEs ? "ECDSA en hardware" : isEn ? "Hardware ECDSA" : "ECDSA em hardware";
|
||||
}
|
||||
if (String(seg.ecc).startsWith("Sim") || String(seg.ecc).startsWith("Yes") || String(seg.ecc).startsWith("Sí")) {
|
||||
return isEs ? "ECC en hardware" : isEn ? "Hardware ECC" : "ECC em hardware";
|
||||
if (String(seg.ecc).startsWith("Sim") || String(seg.ecc).startsWith("Yes") || String(seg.ecc).startsWith("Sí") || String(seg.ecc).startsWith("Oui") || String(seg.ecc).startsWith("Ja")) {
|
||||
return isDe ? "Hardware-ECC" : isFr ? "ECC matériel" : isEs ? "ECC en hardware" : isEn ? "Hardware ECC" : "ECC em hardware";
|
||||
}
|
||||
if (String(seg.aes).startsWith("AES")) return "AES + SHA";
|
||||
return isEs ? "Solo SHA" : isEn ? "SHA only" : "Só SHA";
|
||||
return isDe ? "Nur SHA" : isFr ? "SHA uniquement" : isEs ? "Solo SHA" : isEn ? "SHA only" : "Só SHA";
|
||||
}
|
||||
|
||||
const simPt = (v) => !String(v).includes("Não");
|
||||
const simEn = (v) => !String(v).includes("No") && !String(v).includes("Não");
|
||||
const simEs = (v) => !String(v).includes("No") && !String(v).includes("Não");
|
||||
const simFr = (v) => !String(v).includes("Non") && !String(v).includes("No") && !String(v).includes("Não");
|
||||
const simDe = (v) => !String(v).includes("Nein") && !String(v).includes("No") && !String(v).includes("Não");
|
||||
|
||||
export default function Series() {
|
||||
const pathname = usePathname();
|
||||
const isEn = pathname?.startsWith("/en");
|
||||
const isEs = pathname?.startsWith("/es");
|
||||
const currentLocale = isEs ? "es" : isEn ? "en" : "pt";
|
||||
const seriesData = isEs ? seriesDataEs : isEn ? seriesDataEn : seriesDataPt;
|
||||
const filtros = isEs ? FILTROS_ES : isEn ? FILTROS_EN : FILTROS_PT;
|
||||
const isFr = pathname?.startsWith("/fr");
|
||||
const isDe = pathname?.startsWith("/de");
|
||||
const currentLocale = isDe ? "de" : isFr ? "fr" : isEs ? "es" : isEn ? "en" : "pt";
|
||||
const seriesData = isDe ? seriesDataDe : isFr ? seriesDataFr : isEs ? seriesDataEs : isEn ? seriesDataEn : seriesDataPt;
|
||||
const filtros = isDe ? FILTROS_DE : isFr ? FILTROS_FR : isEs ? FILTROS_ES : isEn ? FILTROS_EN : FILTROS_PT;
|
||||
|
||||
const [filtro, setFiltro] = useState("todas");
|
||||
const todas = Object.entries(seriesData);
|
||||
@@ -84,12 +106,28 @@ export default function Series() {
|
||||
<PageHero
|
||||
isEn={isEn}
|
||||
isEs={isEs}
|
||||
trail={[{ label: isEs ? "Series" : isEn ? "Series" : "Séries" }]}
|
||||
isFr={isFr}
|
||||
isDe={isDe}
|
||||
trail={[{ label: isDe ? "Serien" : isFr ? "Séries" : isEs ? "Series" : isEn ? "Series" : "Séries" }]}
|
||||
icon={Compass}
|
||||
label={isEs ? `${todas.length} familias de Espressif` : isEn ? `${todas.length} Espressif families` : `${todas.length} famílias da Espressif`}
|
||||
title={isEs ? "Series ESP32" : isEn ? "ESP32 Series" : "Séries ESP32"}
|
||||
label={
|
||||
isDe
|
||||
? `${todas.length} Espressif-Familien`
|
||||
: isFr
|
||||
? `${todas.length} familles d'Espressif`
|
||||
: isEs
|
||||
? `${todas.length} familias de Espressif`
|
||||
: isEn
|
||||
? `${todas.length} Espressif families`
|
||||
: `${todas.length} famílias da Espressif`
|
||||
}
|
||||
title={isDe ? "ESP32-Serien" : isFr ? "Séries ESP32" : isEs ? "Series ESP32" : isEn ? "ESP32 Series" : "Séries ESP32"}
|
||||
lede={
|
||||
isEs
|
||||
isDe
|
||||
? "Jede Serie wurde für einen bestimmten Projekttyp entwickelt, von Batteriesensoren bis hin zu KI-Beschleunigern. Öffnen Sie das Datenblatt für Pinbelegung, Peripherie und Sicherheit."
|
||||
: isFr
|
||||
? "Chaque série a été conçue pour un type de projet spécifique, des capteurs sur batterie aux accélérateurs IA. Ouvrez la fiche pour consulter le brochage, les périphériques et la sécurité."
|
||||
: isEs
|
||||
? "Cada serie fue diseñada para un tipo de proyecto específico, desde sensores a batería hasta aceleradores de IA. Abra la ficha para ver patillaje, periféricos y seguridad."
|
||||
: isEn
|
||||
? "Each series was designed for a specific project type, from battery-powered sensors to AI accelerators. Open the sheet to see pinout, peripherals, and security."
|
||||
@@ -97,8 +135,8 @@ export default function Series() {
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="shell pt-10 sm:pt-12" aria-label={isEs ? "Lista de series" : isEn ? "Series list" : "Lista de séries"}>
|
||||
<div role="group" aria-label={isEs ? "Filtrar series" : isEn ? "Filter series" : "Filtrar séries"} className="flex flex-wrap gap-2">
|
||||
<section className="shell pt-10 sm:pt-12" aria-label={isDe ? "Serienliste" : isFr ? "Liste des séries" : isEs ? "Lista de series" : isEn ? "Series list" : "Lista de séries"}>
|
||||
<div role="group" aria-label={isDe ? "Serien filtern" : isFr ? "Filtrer les séries" : isEs ? "Filtrar series" : isEn ? "Filter series" : "Filtrar séries"} className="flex flex-wrap gap-2">
|
||||
{filtros.map((f) => (
|
||||
<button key={f.id} type="button" aria-pressed={filtro === f.id} onClick={() => setFiltro(f.id)} className="chip-btn">
|
||||
{f.rotulo}
|
||||
@@ -106,13 +144,50 @@ export default function Series() {
|
||||
))}
|
||||
</div>
|
||||
<p role="status" aria-live="polite" className="mt-4 text-sm text-muted">
|
||||
{series.length} {series.length === 1 ? (isEs ? "serie" : isEn ? "series" : "série") : (isEs ? "series" : isEn ? "series" : "séries")}
|
||||
{series.length}{" "}
|
||||
{series.length === 1
|
||||
? isDe
|
||||
? "Serie"
|
||||
: isFr
|
||||
? "série"
|
||||
: isEs
|
||||
? "serie"
|
||||
: isEn
|
||||
? "series"
|
||||
: "série"
|
||||
: isDe
|
||||
? "Serien"
|
||||
: isFr
|
||||
? "séries"
|
||||
: isEs
|
||||
? "series"
|
||||
: isEn
|
||||
? "series"
|
||||
: "séries"}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{series.map(([key, serie]) => {
|
||||
const riscv = serie.arquitetura.includes("RISC-V");
|
||||
const fatos = isEs
|
||||
const fatos = isDe
|
||||
? [
|
||||
["Kerne", serie.nucleos],
|
||||
["Taktfrequenz", serie.frequencia],
|
||||
["SRAM", serie.memoria_sram],
|
||||
["Wi-Fi", simDe(serie.wifi) ? "Ja" : "Nein"],
|
||||
["Bluetooth", String(serie.bluetooth).includes("Nein") || String(serie.bluetooth).includes("No") || String(serie.bluetooth).includes("Não") ? "Nein" : "Ja"],
|
||||
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
|
||||
].filter(Boolean)
|
||||
: isFr
|
||||
? [
|
||||
["Cœurs", serie.nucleos],
|
||||
["Fréquence", serie.frequencia],
|
||||
["SRAM", serie.memoria_sram],
|
||||
["Wi-Fi", simFr(serie.wifi) ? "Oui" : "Non"],
|
||||
["Bluetooth", String(serie.bluetooth).includes("Non") || String(serie.bluetooth).includes("No") || String(serie.bluetooth).includes("Não") ? "Non" : "Oui"],
|
||||
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
|
||||
].filter(Boolean)
|
||||
: isEs
|
||||
? [
|
||||
["Núcleos", serie.nucleos],
|
||||
["Frecuencia", serie.frequencia],
|
||||
@@ -150,7 +225,7 @@ export default function Series() {
|
||||
{fatos.map(([rotulo, valor]) => (
|
||||
<div key={rotulo} className="flex items-baseline justify-between gap-4 py-2">
|
||||
<dt className="text-muted">{rotulo}</dt>
|
||||
<dd className={`text-right font-medium ${valor === "Não" || valor === "No" ? "text-muted" : "text-ink"}`}>{valor}</dd>
|
||||
<dd className={`text-right font-medium ${valor === "Não" || valor === "No" || valor === "Non" || valor === "Nein" ? "text-muted" : "text-ink"}`}>{valor}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
@@ -159,14 +234,14 @@ export default function Series() {
|
||||
{serie.aceleradores_ia && (
|
||||
<li className="chip chip-accent">
|
||||
<Sparkles className="h-3 w-3" aria-hidden="true" />
|
||||
{isEs ? "Aceleradores de IA" : isEn ? "AI Accelerators" : "Aceleradores de IA"}
|
||||
{isDe ? "KI-Beschleuniger" : isFr ? "Accélérateurs IA" : isEs ? "Aceleradores de IA" : isEn ? "AI Accelerators" : "Aceleradores de IA"}
|
||||
</li>
|
||||
)}
|
||||
<li className="chip">
|
||||
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
|
||||
{nivelSeguranca(serie, isEn, isEs)}
|
||||
{nivelSeguranca(serie, isEn, isEs, isFr, isDe)}
|
||||
</li>
|
||||
{(serie.matter === "Sim" || serie.matter === "Yes" || serie.matter === "Sí") && (
|
||||
{(serie.matter === "Sim" || serie.matter === "Yes" || serie.matter === "Sí" || serie.matter === "Oui" || serie.matter === "Ja") && (
|
||||
<li className="chip">
|
||||
<Home className="h-3 w-3 text-accent" aria-hidden="true" />
|
||||
Matter
|
||||
@@ -175,13 +250,13 @@ export default function Series() {
|
||||
{serie.status_documentacao && (
|
||||
<li className="chip">
|
||||
<AlertTriangle className="h-3 w-3 text-gold" aria-hidden="true" />
|
||||
{isEs ? "Documentación preliminar" : isEn ? "Preliminary documentation" : "Documentação preliminar"}
|
||||
{isDe ? "Vorläufige Dokumentation" : isFr ? "Documentation préliminaire" : isEs ? "Documentación preliminar" : isEn ? "Preliminary documentation" : "Documentação preliminar"}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-accent">
|
||||
{isEs ? "Abrir ficha" : isEn ? "Open datasheet" : "Abrir ficha"}
|
||||
{isDe ? "Datenblatt öffnen" : isFr ? "Ouvrir la fiche" : isEs ? "Abrir ficha" : isEn ? "Open datasheet" : "Abrir ficha"}
|
||||
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
|
||||
</span>
|
||||
</Link>
|
||||
@@ -194,10 +269,14 @@ export default function Series() {
|
||||
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
|
||||
<div className="max-w-2xl">
|
||||
<h2 className="section-title">
|
||||
{isEs ? "¿Tiene dudas entre dos series?" : isEn ? "Unsure between two series?" : "Ficou em dúvida entre duas séries?"}
|
||||
{isDe ? "Unsicher zwischen zwei Serien?" : isFr ? "Vous hésitez entre deux séries ?" : isEs ? "¿Tiene dudas entre dos series?" : isEn ? "Unsure between two series?" : "Ficou em dúvida entre duas séries?"}
|
||||
</h2>
|
||||
<p className="mt-3 text-base text-muted">
|
||||
{isEs
|
||||
{isDe
|
||||
? "Der Vergleicher stellt bis zu vier Modelle gegenüber: Kerne, Funk, Speicher, Peripherie und Sicherheit."
|
||||
: isFr
|
||||
? "Le comparateur place jusqu'à quatre modèles côte à côte : cœurs, radio, mémoire, périphériques et sécurité."
|
||||
: isEs
|
||||
? "El comparador coloca hasta cuatro modelos lado a lado: núcleos, radio, memoria, periféricos y seguridad."
|
||||
: isEn
|
||||
? "The comparator tool puts up to four models side by side: cores, radio, memory, peripherals, and security."
|
||||
@@ -206,7 +285,7 @@ export default function Series() {
|
||||
</div>
|
||||
<Link href={getLocalizedHref("/comparacao", currentLocale)} className="btn btn-primary shrink-0">
|
||||
<Scale className="h-4 w-4" aria-hidden="true" />
|
||||
{isEs ? "Comparar series" : isEn ? "Compare series" : "Comparar séries"}
|
||||
{isDe ? "Serien vergleichen" : isFr ? "Comparer les séries" : isEs ? "Comparar series" : isEn ? "Compare series" : "Comparar séries"}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user