diff --git a/app/[locale]/page.jsx b/app/[locale]/page.jsx
index 1fbb4a1..70d28fa 100644
--- a/app/[locale]/page.jsx
+++ b/app/[locale]/page.jsx
@@ -5,7 +5,7 @@ import Link from "next/link";
import seriesData from "@/public/series.json";
import frameworksData from "@/public/frameworks.json";
import { SITE } from "@/lib/seo";
-import { getLocalizedHref } from "@/components/LanguageSelector";
+import { getLocalizedHref } from "@/i18n/links";
import {
AlertTriangle,
ArrowRight,
diff --git a/components/Footer.jsx b/components/Footer.jsx
index 821c88d..1263955 100644
--- a/components/Footer.jsx
+++ b/components/Footer.jsx
@@ -6,16 +6,16 @@ import Logo from "@/components/Logo";
import GiteaMark from "@/components/GiteaMark";
import { ArrowUpRight } from "lucide-react";
import { SITE } from "@/lib/seo";
-import { LOCALES, getLocalizedHref } from "@/components/LanguageSelector";
+import { LOCALES, getLocalizedHref } from "@/i18n/links";
-const BASE_COLUMNS = [
+const COLUMNS = [
{
title: "Explorar",
links: [
{ href: "/series", label: "Séries ESP32" },
{ href: "/comparacao", label: "Comparador" },
{ href: "/catalogo", label: "Catálogo de placas" },
- { href: "/livro", label: "O livro", livroOnly: true },
+ { href: "/livro", label: "O livro" },
],
},
{
@@ -48,9 +48,9 @@ export default function Footer() {
const getLinkHref = (href) => getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
- const columns = BASE_COLUMNS.map((col) => ({
- ...col,
- links: col.links.filter((l) => !l.livroOnly || isPt),
+ const columns = COLUMNS.map((column) => ({
+ ...column,
+ links: column.links.filter((link) => link.href !== "/livro" || isPt),
}));
return (
@@ -139,40 +139,45 @@ export default function Footer() {
Ciência Embarcada
-
-
-
© {year} ESPDocs · Conteúdo sob licença MIT. Feito para a comunidade de sistemas embarcados.
-
diff --git a/components/Header.jsx b/components/Header.jsx
index 309fa63..5372f9c 100644
--- a/components/Header.jsx
+++ b/components/Header.jsx
@@ -5,7 +5,8 @@ import { createPortal } from "react-dom";
import Link from "next/link";
import Logo from "@/components/Logo";
import SearchDialog from "@/components/SearchDialog";
-import LanguageSelector, { LOCALES, getLocalizedHref } from "@/components/LanguageSelector";
+import LanguageSelector from "@/components/LanguageSelector";
+import { LOCALES, getLocalizedHref } from "@/i18n/links";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import {
diff --git a/components/LanguageSelector.jsx b/components/LanguageSelector.jsx
index 5d43e9a..ba81f97 100644
--- a/components/LanguageSelector.jsx
+++ b/components/LanguageSelector.jsx
@@ -3,131 +3,9 @@
import { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import { ChevronDown, Globe } from "lucide-react";
+import { LOCALES, getLocalizedHref } from "@/i18n/links";
-export const LOCALES = [
- { code: "pt", label: "Português", short: "PT", lang: "pt-BR" },
- { code: "en", label: "English", short: "EN", lang: "en" },
- { code: "es", label: "Español", short: "ES", lang: "es-419" },
- { code: "fr", label: "Français", short: "FR", lang: "fr" },
- { code: "de", label: "Deutsch", short: "DE", lang: "de" },
- { code: "zh-CN", label: "简体中文", short: "中", lang: "zh-CN" },
-];
-
-const CANONICAL_ROUTES = [
- {
- id: "series_item",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:series|serien)\/([^/]+)/,
- getSlug: (loc, key) => (loc === "de" ? `/serien/${key}` : `/series/${key}`),
- },
- {
- id: "frameworks_item",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/frameworks\/([^/]+)/,
- getSlug: (loc, key) => `/frameworks/${key}`,
- },
- {
- id: "series",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:series|serien)(?:\/|$)/,
- getSlug: (loc) => (loc === "de" ? "/serien" : "/series"),
- },
- {
- id: "catalogo",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:catalogo|catalog|catalogue|katalog)(?:\/|$)/,
- getSlug: (loc) => {
- if (loc === "de") return "/katalog";
- if (loc === "fr") return "/catalogue";
- if (loc === "en" || loc === "zh-CN") return "/catalog";
- return "/catalogo";
- },
- },
- {
- id: "comparacao",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:comparacao|compare|comparacion|comparaison|vergleich)(?:\/|$)/,
- getSlug: (loc) => {
- if (loc === "en" || loc === "zh-CN") return "/compare";
- if (loc === "es") return "/comparacion";
- if (loc === "fr") return "/comparaison";
- if (loc === "de") return "/vergleich";
- return "/comparacao";
- },
- },
- {
- id: "componentes",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:componentes|components|composants|komponenten)(?:\/|$)/,
- getSlug: (loc) => {
- if (loc === "en" || loc === "zh-CN") return "/components";
- if (loc === "fr") return "/composants";
- if (loc === "de") return "/komponenten";
- return "/componentes";
- },
- },
- {
- id: "diagnostico",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:diagnostico|diagnostics|diagnostic|diagnose)(?:\/|$)/,
- getSlug: (loc) => {
- if (loc === "en" || loc === "zh-CN") return "/diagnostics";
- if (loc === "fr") return "/diagnostic";
- if (loc === "de") return "/diagnose";
- return "/diagnostico";
- },
- },
- {
- id: "frameworks",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/frameworks(?:\/|$)/,
- getSlug: () => "/frameworks",
- },
- {
- id: "glossario",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:glossario|glossary|glosario|glossaire|glossar)(?:\/|$)/,
- getSlug: (loc) => {
- if (loc === "en" || loc === "zh-CN") return "/glossary";
- if (loc === "es") return "/glosario";
- if (loc === "fr") return "/glossaire";
- if (loc === "de") return "/glossar";
- return "/glossario";
- },
- },
- {
- id: "seletor",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:seletor|selector|selecteur|auswahl)(?:\/|$)/,
- getSlug: (loc) => {
- if (loc === "en" || loc === "es" || loc === "zh-CN") return "/selector";
- if (loc === "fr") return "/selecteur";
- if (loc === "de") return "/auswahl";
- return "/seletor";
- },
- },
- {
- id: "sobre",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:sobre|about|acerca|a-propos|ueber-uns)(?:\/|$)/,
- getSlug: (loc) => {
- if (loc === "en" || loc === "zh-CN") return "/about";
- if (loc === "es") return "/acerca";
- if (loc === "fr") return "/a-propos";
- if (loc === "de") return "/ueber-uns";
- return "/sobre";
- },
- },
- {
- id: "livro",
- match: /^\/(?:pt|en|es|fr|de|zh-CN)\/livro(?:\/|$)/,
- getSlug: (loc) => (loc === "pt" ? "/livro" : ""),
- },
-];
-
-export function getLocalizedHref(pathname, targetLocale) {
- if (!pathname || pathname === "/" || pathname === `/${targetLocale}` || pathname === `/${targetLocale}/`) {
- return `/${targetLocale}/`;
- }
-
- for (const route of CANONICAL_ROUTES) {
- const m = pathname.match(route.match);
- if (m) {
- const slug = route.getSlug(targetLocale, m[1]);
- return slug ? `/${targetLocale}${slug}` : `/${targetLocale}/`;
- }
- }
- return `/${targetLocale}/`;
-}
+export { LOCALES, getLocalizedHref };
export default function LanguageSelector() {
const pathname = usePathname() || "";
diff --git a/i18n/links.js b/i18n/links.js
new file mode 100644
index 0000000..9ff221e
--- /dev/null
+++ b/i18n/links.js
@@ -0,0 +1,129 @@
+/**
+ * Utilitários de rotas e navegação internacionalizada.
+ * Módulo puro (sem 'use client') seguro para Server e Client Components.
+ */
+
+export const LOCALES = [
+ { code: "pt", label: "Português", short: "PT", lang: "pt-BR" },
+ { code: "en", label: "English", short: "EN", lang: "en" },
+ { code: "es", label: "Español", short: "ES", lang: "es-419" },
+ { code: "fr", label: "Français", short: "FR", lang: "fr" },
+ { code: "de", label: "Deutsch", short: "DE", lang: "de" },
+ { code: "zh-CN", label: "简体中文", short: "中", lang: "zh-CN" },
+];
+
+export const CANONICAL_ROUTES = [
+ {
+ id: "series_item",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:series|serien)\/([^/]+)/,
+ getSlug: (loc, key) => (loc === "de" ? `/serien/${key}` : `/series/${key}`),
+ },
+ {
+ id: "frameworks_item",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/frameworks\/([^/]+)/,
+ getSlug: (loc, key) => `/frameworks/${key}`,
+ },
+ {
+ id: "series",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:series|serien)(?:\/|$)/,
+ getSlug: (loc) => (loc === "de" ? "/serien" : "/series"),
+ },
+ {
+ id: "catalogo",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:catalogo|catalog|catalogue|katalog)(?:\/|$)/,
+ getSlug: (loc) => {
+ if (loc === "de") return "/katalog";
+ if (loc === "fr") return "/catalogue";
+ if (loc === "en" || loc === "zh-CN") return "/catalog";
+ return "/catalogo";
+ },
+ },
+ {
+ id: "comparacao",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:comparacao|compare|comparacion|comparaison|vergleich)(?:\/|$)/,
+ getSlug: (loc) => {
+ if (loc === "en" || loc === "zh-CN") return "/compare";
+ if (loc === "es") return "/comparacion";
+ if (loc === "fr") return "/comparaison";
+ if (loc === "de") return "/vergleich";
+ return "/comparacao";
+ },
+ },
+ {
+ id: "componentes",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:componentes|components|composants|komponenten)(?:\/|$)/,
+ getSlug: (loc) => {
+ if (loc === "en" || loc === "zh-CN") return "/components";
+ if (loc === "fr") return "/composants";
+ if (loc === "de") return "/komponenten";
+ return "/componentes";
+ },
+ },
+ {
+ id: "diagnostico",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:diagnostico|diagnostics|diagnostic|diagnose)(?:\/|$)/,
+ getSlug: (loc) => {
+ if (loc === "en" || loc === "zh-CN") return "/diagnostics";
+ if (loc === "fr") return "/diagnostic";
+ if (loc === "de") return "/diagnose";
+ return "/diagnostico";
+ },
+ },
+ {
+ id: "frameworks",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/frameworks(?:\/|$)/,
+ getSlug: () => "/frameworks",
+ },
+ {
+ id: "glossario",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:glossario|glossary|glosario|glossaire|glossar)(?:\/|$)/,
+ getSlug: (loc) => {
+ if (loc === "en" || loc === "zh-CN") return "/glossary";
+ if (loc === "es") return "/glosario";
+ if (loc === "fr") return "/glossaire";
+ if (loc === "de") return "/glossar";
+ return "/glossario";
+ },
+ },
+ {
+ id: "seletor",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:seletor|selector|selecteur|auswahl)(?:\/|$)/,
+ getSlug: (loc) => {
+ if (loc === "en" || loc === "es" || loc === "zh-CN") return "/selector";
+ if (loc === "fr") return "/selecteur";
+ if (loc === "de") return "/auswahl";
+ return "/seletor";
+ },
+ },
+ {
+ id: "sobre",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/(?:sobre|about|acerca|a-propos|ueber-uns)(?:\/|$)/,
+ getSlug: (loc) => {
+ if (loc === "en" || loc === "zh-CN") return "/about";
+ if (loc === "es") return "/acerca";
+ if (loc === "fr") return "/a-propos";
+ if (loc === "de") return "/ueber-uns";
+ return "/sobre";
+ },
+ },
+ {
+ id: "livro",
+ match: /^\/(?:pt|en|es|fr|de|zh-CN)\/livro(?:\/|$)/,
+ getSlug: (loc) => (loc === "pt" ? "/livro" : ""),
+ },
+];
+
+export function getLocalizedHref(pathname, targetLocale) {
+ if (!pathname || pathname === "/" || pathname === `/${targetLocale}` || pathname === `/${targetLocale}/`) {
+ return `/${targetLocale}/`;
+ }
+
+ for (const route of CANONICAL_ROUTES) {
+ const m = pathname.match(route.match);
+ if (m) {
+ const slug = route.getSlug(targetLocale, m[1]);
+ return slug ? `/${targetLocale}${slug}` : `/${targetLocale}/`;
+ }
+ }
+ return `/${targetLocale}/`;
+}