From f4789f766506d80ce1e1d96beab85907261620eb Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 19:16:11 -0300 Subject: [PATCH 01/25] feat(i18n): add Spanish and Brazilian Portuguese locales Wires VitePress i18n via the locales config and mirrors the four highest-traffic pages under docs/es/ and docs/pt-BR/: - what-is-libremesh - getting-started - guide/connecting - features Other pages link out to English with a (en)/(em) hint, per the link-out convention in docs/TRANSLATING.md. Sidebars for the new locales are trimmed to translated pages only; they grow as more translations land. Adds a first-visit LanguageBanner that reads navigator.language and, on the first visit, suggests the matching locale. Choice is remembered in localStorage so it does not reappear. Banner is mounted via the layout-top slot in theme/index.ts and styles live in style.css. Detected locales currently: es, pt-BR. docs/TRANSLATING.md is the contributor-facing guide: layout, glossary of terms to keep in English (Batman-adv, Babel, LiMe, LuCI, OpenWrt, ...), link-out convention, and how to add a new language (one new folder + one config entry + one message entry in LanguageBanner.vue). Refs #172. --- docs/.vitepress/config.mts | 142 +++++++++++++++++++---- docs/.vitepress/theme/LanguageBanner.vue | 83 +++++++++++++ docs/.vitepress/theme/index.ts | 4 +- docs/.vitepress/theme/style.css | 54 +++++++++ docs/TRANSLATING.md | 65 +++++++++++ docs/es/features.md | 13 +++ docs/es/getting-started.md | 79 +++++++++++++ docs/es/guide/connecting.md | 93 +++++++++++++++ docs/es/what-is-libremesh.md | 117 +++++++++++++++++++ docs/pt-BR/features.md | 13 +++ docs/pt-BR/getting-started.md | 79 +++++++++++++ docs/pt-BR/guide/connecting.md | 93 +++++++++++++++ docs/pt-BR/what-is-libremesh.md | 117 +++++++++++++++++++ 13 files changed, 926 insertions(+), 26 deletions(-) create mode 100644 docs/.vitepress/theme/LanguageBanner.vue create mode 100644 docs/TRANSLATING.md create mode 100644 docs/es/features.md create mode 100644 docs/es/getting-started.md create mode 100644 docs/es/guide/connecting.md create mode 100644 docs/es/what-is-libremesh.md create mode 100644 docs/pt-BR/features.md create mode 100644 docs/pt-BR/getting-started.md create mode 100644 docs/pt-BR/guide/connecting.md create mode 100644 docs/pt-BR/what-is-libremesh.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index e87b1c07..fc538950 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -21,7 +21,38 @@ export default defineConfig({ locales: { root: { label: 'English', - lang: 'en' + lang: 'en', + themeConfig: { + nav: navEn(), + sidebar: { + '/': { base: '/', items: sidebarGuideEn() }, + '/reference/': { base: '/reference/', items: sidebarReferenceEn() } + }, + }, + }, + es: { + label: 'Español', + lang: 'es', + link: '/es/', + themeConfig: { + nav: navEs(), + sidebar: { + '/es/': { base: '/es/', items: sidebarGuideEs() }, + '/es/reference/': { base: '/es/reference/', items: sidebarReferenceEn() } + }, + }, + }, + 'pt-BR': { + label: 'Português (BR)', + lang: 'pt-BR', + link: '/pt-BR/', + themeConfig: { + nav: navPtBr(), + sidebar: { + '/pt-BR/': { base: '/pt-BR/', items: sidebarGuidePtBr() }, + '/pt-BR/reference/': { base: '/pt-BR/reference/', items: sidebarReferenceEn() } + }, + }, }, }, @@ -45,13 +76,6 @@ export default defineConfig({ }, outline: 'deep', - nav: nav(), - - sidebar: { - '/': { base: '/', items: sidebarGuide() }, - '/reference/': { base: '/reference/', items: sidebarReference() } - }, - socialLinks: [ { icon: 'github', link: 'https://github.com/libremesh/lime-packages' }, // { icon: 'maildotru', link: 'https://www.autistici.org/mailman/listinfo/libremesh' }, @@ -60,16 +84,12 @@ export default defineConfig({ { icon: 'peertube', link: 'https://media.exo.cat/a/libremesh' } ], }, - // async buildEnd() { - // // const meetings = await createContentLoader('meetings/*.md').load() - // // generate files based on posts metadata, e.g. RSS feed - // } }) -function nav(): DefaultTheme.NavItem[] { +function navEn(): DefaultTheme.NavItem[] { return [ { text: 'Guide', link: '/guide/packages-selection' }, - { text: 'Reference', link: '/reference/configuration' }, + { text: 'Reference', link: '/reference/configuration' }, { text: 'News', items: [ { text: 'v2024.1', link: '/news/2025-05-04' }, @@ -82,7 +102,39 @@ function nav(): DefaultTheme.NavItem[] { ] } -function sidebarGuide(): DefaultTheme.SidebarItem[] { +function navEs(): DefaultTheme.NavItem[] { + return [ + { text: 'Guía', link: '/es/guide/packages-selection' }, + { text: 'Referencia', link: '/es/reference/configuration' }, + { text: 'Noticias', + items: [ + { text: 'v2024.1', link: '/es/news/2025-05-04' }, + { text: 'v2020.4', link: '/es/news/2023-10-07' }, + { text: 'Artículos recientes', link: '/es/news/' }, + { text: 'Changelog', link: '/es/changelog'}, + { text: 'Issues', link: 'https://github.com/libremesh/lime-packages/issues'}, + ] + }, + ] +} + +function navPtBr(): DefaultTheme.NavItem[] { + return [ + { text: 'Guia', link: '/pt-BR/guide/packages-selection' }, + { text: 'Referência', link: '/pt-BR/reference/configuration' }, + { text: 'Notícias', + items: [ + { text: 'v2024.1', link: '/pt-BR/news/2025-05-04' }, + { text: 'v2020.4', link: '/pt-BR/news/2023-10-07' }, + { text: 'Artigos recentes', link: '/pt-BR/news/' }, + { text: 'Changelog', link: '/pt-BR/changelog'}, + { text: 'Issues', link: 'https://github.com/libremesh/lime-packages/issues'}, + ] + }, + ] +} + +function sidebarGuideEn(): DefaultTheme.SidebarItem[] { return [ { text: 'Introduction', @@ -98,13 +150,13 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { collapsed: false, items: [ { text: 'Connecting to the router', link: '/guide/connecting' }, - { text: 'Packages selection', - link: '/guide/packages-selection', + { text: 'Packages selection', + link: '/guide/packages-selection', collapsed: true, items: [ { text: 'Network Profiles', link: '/guide/network-profiles' } ] }, - { text: 'Build LibreMesh', + { text: 'Build LibreMesh', link: '/build/', collapsed: true, items: [ @@ -122,8 +174,8 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { items: [ { text: 'Testing Guide', link: '/development/testing' }, { text: 'Run LibreMesh on QEMU', link: '/development/virtualizing' }, - { text: 'Hacking', - link: '/development/hacking', + { text: 'Hacking', + link: '/development/hacking', collapsed: true, items: [ { text: 'Kernel Vermagic', link: '/development/hacking/kernel_vermagic' } @@ -147,7 +199,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { items: [ { text: 'Media list', link: '/resources/media_list' }, { text: 'Related projects', link: '/resources/related_projects' }, - { text: 'Tools', + { text: 'Tools', link: '/resources/tools/monitoring', collapsed: true, items: [ @@ -170,14 +222,54 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { { text: 'Communities', items: generateSidebarItems(communities) }, { text: 'Packages', items: generateSidebarItems(profiles), }, ] + }, + ] +} + +function sidebarGuideEs(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Introducción', + collapsed: false, + items: [ + { text: '¿Qué es LibreMesh?', link: '/es/what-is-libremesh' }, + { text: 'Primeros pasos', link: '/es/getting-started' }, + { text: 'Características', link: '/es/features' }, + ] + }, + { + text: 'Guía de uso', + collapsed: false, + items: [ + { text: 'Conectarse al router', link: '/es/guide/connecting' }, + ] + }, + ] +} - +function sidebarGuidePtBr(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Introdução', + collapsed: false, + items: [ + { text: 'O que é o LibreMesh?', link: '/pt-BR/what-is-libremesh' }, + { text: 'Primeiros passos', link: '/pt-BR/getting-started' }, + { text: 'Recursos', link: '/pt-BR/features' }, + ] + }, + { + text: 'Guia de uso', + collapsed: false, + items: [ + { text: 'Conectando ao roteador', link: '/pt-BR/guide/connecting' }, + ] }, ] } -function sidebarReference(): DefaultTheme.SidebarItem[] { +function sidebarReferenceEn(): DefaultTheme.SidebarItem[] { return [ { text: 'Reference', @@ -185,7 +277,7 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { { text: 'Configuration', link: 'configuration' }, { text: 'lime-config', link: 'lime-config' }, { text: 'Flavors', link: 'flavors' }, - { text: 'Default protocols', + { text: 'Default protocols', collapsed: true, items: [ { text: 'Batman-adv', link: 'network/protocols/batman-adv'}, @@ -207,7 +299,7 @@ function sidebarReference(): DefaultTheme.SidebarItem[] { { text: 'Interface specific', link: 'wifi/interface-specific'}, ] }, { text: 'Generic UCI configs', link: 'generic_config' }, - { text: 'Hardware detection', + { text: 'Hardware detection', items: [ { text: 'Ground Routing', link: 'hardware_detection/ground_routing' }, { text: 'Watchcat', link: 'hardware_detection/watchcat' } diff --git a/docs/.vitepress/theme/LanguageBanner.vue b/docs/.vitepress/theme/LanguageBanner.vue new file mode 100644 index 00000000..a22d43ae --- /dev/null +++ b/docs/.vitepress/theme/LanguageBanner.vue @@ -0,0 +1,83 @@ + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 210fda47..ae4ed0a4 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -5,13 +5,15 @@ import DefaultTheme from 'vitepress/theme' import './style.css' import { redirects } from './redirects' import LayoutWide from './LayoutWide.vue' +import LanguageBanner from './LanguageBanner.vue' export default { extends: DefaultTheme, Layout: () => { return h(DefaultTheme.Layout, null, { // https://vitepress.dev/guide/extending-default-theme#layout-slots - 'wide': () => h(LayoutWide) + 'wide': () => h(LayoutWide), + 'layout-top': () => h(LanguageBanner) }) }, enhanceApp({ app, router, siteData }) { diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index cd7f2a0b..f542ee6e 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -139,3 +139,57 @@ ); } +/** + * Component: Language suggestion banner + * -------------------------------------------------------------------------- */ + +.lang-banner { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 0.75rem 1.25rem; + padding: 0.6rem 1.25rem; + background: var(--vp-c-brand-soft); + color: var(--vp-c-text-1); + font-size: 0.9rem; + border-bottom: 1px solid var(--vp-c-divider); + text-align: center; +} + +.lang-banner__msg strong { + font-weight: 600; +} + +.lang-banner__actions { + display: inline-flex; + gap: 0.5rem; +} + +.lang-banner__btn { + appearance: none; + border: 1px solid var(--vp-c-divider); + background: var(--vp-c-bg); + color: var(--vp-c-text-1); + padding: 0.25rem 0.75rem; + font-size: 0.85rem; + border-radius: 6px; + cursor: pointer; + font-family: inherit; +} + +.lang-banner__btn:hover { + border-color: var(--vp-c-brand-1); +} + +.lang-banner__btn--primary { + background: var(--vp-c-brand-3); + color: var(--vp-c-white); + border-color: var(--vp-c-brand-3); +} + +.lang-banner__btn--primary:hover { + background: var(--vp-c-brand-2); + border-color: var(--vp-c-brand-2); +} + diff --git a/docs/TRANSLATING.md b/docs/TRANSLATING.md new file mode 100644 index 00000000..af696bb9 --- /dev/null +++ b/docs/TRANSLATING.md @@ -0,0 +1,65 @@ +# Translating LibreMesh docs + +Thanks for helping translate the LibreMesh website. This page is a quick reference for contributors. Keep it short — most of what we need is just consistency. + +## How translations are organized + +Each non-English language has its own folder under `docs/`: + +``` +docs/ +├─ es/ # Español +├─ pt-BR/ # Português (Brasil) +└─ ... # English (the source of truth) lives at docs/ root +``` + +The folder mirrors the English tree. To start a new page in Spanish, copy the English file into `docs/es/...` and translate it. Keep the same filename, same front matter keys, same heading levels. + +## What to translate, what to leave alone + +**Translate:** +- Headings, body prose, UI strings, link titles. +- The English `title:` in the front matter. + +**Keep in English (do not translate):** +- `Batman-adv`, `Babel`, `BMX7`, `OLSR` — protocol names. +- `LiMe`, `LibreMesh`, `OpenWrt`, `LuCI`, `lime-app`, `thisnode.info`, `anygw`, `sysupgrade`, `factory` image, `squashfs-sysupgrade` — product / firmware terms. +- CLI commands, file paths, package names, code blocks. +- Brand names: `GitHub`, `Mastodon`, `Matrix`, `PeerTube`. + +**Tone:** direct, second person ("you"), no marketing speak. Match the English source's register — the original is intentionally plain. + +## What about pages that aren't translated yet? + +We **link out** to the English version with a small hint. For example, in `docs/es/getting-started.md`: + +```md +Consulta la página [Conectarse al router (en)](/guide/connecting) +``` + +The `(en)` / `(em)` suffix tells the reader the link goes to English. This keeps the localized page readable without forcing you to translate every internal link. + +Do not invent `/es/...` paths for pages that don't exist in Spanish yet — they will 404. + +## Adding a new language + +1. Add a new `docs//` folder. +2. Add a `locales` entry in `docs/.vitepress/config.mts` with `label`, `lang`, `link`, and a `themeConfig` with localized `nav` and `sidebar`. Mirror the structure of the existing `es` / `pt-BR` entries. +3. Add the language code to the detection list in `docs/.vitepress/theme/LanguageBanner.vue` (`pickLocale` and `messages`) so first-visit visitors see the suggestion. +4. Add the new locale to this guide's "What to keep in English" list if it introduces new untranslated brand terms. + +## First-visit language banner + +`LanguageBanner.vue` reads `navigator.language` and, on the first visit (no `localStorage` entry), shows a non-intrusive banner suggesting the localized site. The user choice is stored under `libremesh-lang-pref`: + +- `dismissed` — they declined; do not show again. +- `es` / `pt-BR` — they accepted; the stored value is informational only (the actual locale is determined by the URL the user navigates to via the language switcher in the nav). + +## Reviewing a translation PR + +A native speaker should review every translation PR before merge. Things to check: + +- Technical accuracy against the English source. +- Glossary consistency with the list above. +- No broken links: every link should either point to an existing `//...` path or carry the `(en)` / `(em)` hint. +- The build still passes: `pnpm install && pnpm build`. diff --git a/docs/es/features.md b/docs/es/features.md new file mode 100644 index 00000000..336cbd2d --- /dev/null +++ b/docs/es/features.md @@ -0,0 +1,13 @@ +--- +outline: deep +--- + +# Características + +## Modularidad + +Con una única base de código, LibreMesh ofrece una configuración por defecto razonable para los protocolos mesh más comunes: `Babel`, `B.A.T.M.A.N. advanced`, `BMX7`, `OLSR`. + +Puede usarse para unirse a una red mesh existente con una configuración mínima. + +Puede usarse como banco de pruebas para configuraciones con distintos protocolos. diff --git a/docs/es/getting-started.md b/docs/es/getting-started.md new file mode 100644 index 00000000..d472c931 --- /dev/null +++ b/docs/es/getting-started.md @@ -0,0 +1,79 @@ +# Primeros pasos + +## Instalación + +### Requisitos +Consulta la [Tabla de hardware (Table of Hardware)](https://toh.openwrt.org) para ver si tu dispositivo es compatible con OpenWrt. + +::: tip NOTA +Se recomienda que el router tenga **al menos**: + - 16 MB de memoria flash y 128 MB de RAM. + - 1 radio funcionando a 2.4 GHz y 1 a 5 GHz + +Es posible instalar LibreMesh en routers con 8 MB de flash y 64 MB de RAM +Lee el [`8/64 warning`](https://openwrt.org/supported_devices/864_warning) de OpenWrt y consulta la página [selección de paquetes (en)](/guide/packages-selection) para personalizar la compilación. +::: + +::: warning ATENCIÓN +Asegúrate de haber leído la página de la wiki de OpenWrt correspondiente a tu dispositivo. +Lee las instrucciones de instalación y comprueba que dispones del hardware necesario —si hace falta— para instalar el firmware, como un [adaptador USB-UART/Serie](https://openwrt.org/docs/guide-user/installation/generic.flashing.serial) y/o un +[adaptador USB-JTAG](https://openwrt.org/docs/techref/hardware/port.jtag) +::: + +### Descargar el firmware +--- + +**Firmware Selector** +El Firmware Selector solicita una compilación de firmware a través de una instancia de [`ASU`](https://github.com/openwrt/asu) (ImageBuilder en línea). + +https://firmware-selector.libremesh.org + +--- + +**Versiones precompiladas** +Archivo de lanzamientos antiguos con firmwares precompilados mediante Buildroot. + +https://firmware-libremesh.antennine.org + +--- + +**Compila LibreMesh en tu equipo** +Consulta la página [`Compilar LibreMesh` (en)](/build/) para ver las instrucciones de cómo compilar LibreMesh en tu equipo. + +--- + +### Instalar el firmware +Instala el firmware en tu dispositivo siguiendo el método de instalación indicado en la [wiki de OpenWrt](https://openwrt.org) +o, si no aparece, busca las instrucciones en el mensaje de **`git-commit`** dejado por quien añadió el soporte para ese modelo de dispositivo. Consulta la [Tabla de hardware](https://toh.openwrt.org). + +::: tip NOTA +Si tu dispositivo trae el firmware de fábrica, se recomienda **instalar OpenWrt primero**: +::: + +1. Descarga el último firmware `stable` para tu dispositivo desde el [`OpenWrt Firmware Selector`](https://firmware-selector.openwrt.org). + Usa la imagen `factory` para la primera instalación. Consulta [Factory Install: First Time Installation (en)](https://openwrt.org/docs/guide-quick-start/factory_installation): +2. Comprueba que el dispositivo con OpenWrt arranca y funciona correctamente. + Ten en cuenta que OpenWrt por defecto no enciende la Wi-Fi. + Actívala desde `LuCI` en el menú `Network` / `Wireless`. +3. Actualiza a LibreMesh usando una imagen `squashfs-sysupgrade.bin`: + - Sube el firmware a través de la interfaz web `LuCI` desde el menú `System` / `Backup / Flash Firmware`. + - O instálalo por SSH usando el comando `sysupgrade -n firmware.bin`. + + +## Conectarse al router +Se puede acceder al router por web en http://thisnode.info. +Consulta la página [Conectarse al router (en)](/guide/connecting) para ver las opciones detalladas y la resolución de problemas. + + +## Configuración +LibreMesh trae un [sabor por defecto (en)](/reference/flavors) que funciona sin más, sin necesidad de configuración manual. + +Consulta la página de [configuración (en)](/reference/configuration) para ver las opciones detalladas. + + +## Mantenimiento +Instala versiones estables más recientes de OpenWrt para mantener el dispositivo actualizado: +- Suscríbete al boletín [`OpenWrt Announce`](https://lists.openwrt.org/mailman/listinfo/openwrt-announce). +- O sigue al [`OpenWrt Announcement-Bot`](https://social.tchncs.de/@openwrt) en Mastodon. + +Consulta la página [Actualizar (en)](/guide/upgrade) para ver las operaciones recomendadas con LibreMesh. diff --git a/docs/es/guide/connecting.md b/docs/es/guide/connecting.md new file mode 100644 index 00000000..75c72eeb --- /dev/null +++ b/docs/es/guide/connecting.md @@ -0,0 +1,93 @@ +# Conectarse al router + +## desde el navegador web +En el FQDN del anygw por defecto http://thisnode.info. + +Estas direcciones también están disponibles: + - http://10.13.0.1 - anygw por defecto ipv4 + - http://10.13.x.x - dirección ipv4 por defecto del nodo en la LAN `br-lan` + - http://meuno.info - otro FQDN del anygw + - http://minodo.info - otro FQDN del anygw + - http://\ - `hostname` del dispositivo + - http://\.thisnode.info - FQDN del dispositivo + - http://\[fd0d:fe46:8ce8::1\] - anygw por defecto ipv6 + - http://\[fd0d:fe46:8ce8::x\:xx\] - dirección ipv6 por defecto del nodo en la LAN `br-lan` + +![lime-app](/lime-app.png) + +## por SSH + +Consulta [SSH a OpenWrt (en)](https://openwrt.org/docs/guide-quick-start/sshadministration) + +Se recomiendan los siguientes alias de bash: +- `+ssh-rsa` - (opcional) Necesario si el nodo ejecuta una versión de OpenWrt anterior a la rama 23.05 +- `ussh` - un alias para: + - Ignorar los archivos de hosts conocidos, útil al conectarse a la dirección del Anygw `10.13.0.1` o al FQDN del anygw `thisnode.info` + - Desactivar `StrictHostKeyChecking`, útil para reconectar tras actualizar a una versión más reciente de OpenWrt sin "conservar la configuración". + +```sh +cat << EOF >> ~/.bashrc +alias ssh="ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa" +alias ussh="ssh -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" +EOF +source ~/.bashrc +``` + +Conéctate al dispositivo +```sh +ussh root@thisnode.info +``` + +Una vez establecida la conexión, se imprimirán los banners por defecto en la consola: +``` +BusyBox v1.37.0 (2026-03-05 17:27:01 UTC) built-in shell (ash) + + _______ ________ __ + | |.-----.-----.-----.| | | |.----.| |_ + | - || _ | -__| || | | || _|| _| + |_______|| __|_____|__|__||________||__| |____| + |__| W I R E L E S S F R E E D O M + ----------------------------------------------------- + OpenWrt 25.12.0, r32713-f919e7899d Dave's Guitar + ----------------------------------------------------- + + === WARNING! ===================================== + There is no root password defined on this device! + Use the "passwd" command to set up a new password + in order to prevent unauthorized SSH logins. + -------------------------------------------------- + + ___ __ __ _______ __ + | |_|__| |--.----.-----.| | |-----.-----| |--. + | | | _ | _| -__|| | -__|__ --| | + |_____|__|_____|__| |_____||__|_|__|_____|_____|__|__| + + ------------------------------------------------------ + LiMe master development (master rev. 7929208 20260304_1752) + ------------------------------------------------------ + https://libremesh.org + ------------------------------------------------------ + + === System Notes ================================================= + + = edit via http://thisnode.info/app/#/notes or /etc/banner.notes = + + + OpenWrt recently switched to the "apk" package manager! + + OPKG Command APK Equivalent Description + ------------------------------------------------------------------ + opkg install apk add Install a package + opkg remove apk del Remove a package + opkg upgrade apk upgrade Upgrade all packages + opkg files apk info -L List package contents + opkg list-installed apk info List installed packages + opkg update apk update Update package lists + opkg search apk search Search for packages + ------------------------------------------------------------------ + +For more information visit: +https://openwrt.org/docs/guide-user/additional-software/opkg-to-apk-cheatsheet + +root@openwrt:~# +``` diff --git a/docs/es/what-is-libremesh.md b/docs/es/what-is-libremesh.md new file mode 100644 index 00000000..7ad793f5 --- /dev/null +++ b/docs/es/what-is-libremesh.md @@ -0,0 +1,117 @@ +--- +outline: deep +--- + +# ¿Qué es LibreMesh? + +## Objetivos a alcanzar + + - Permitir a usuarios no expertos montar una red mesh y realizar la configuración y gestión básicas desde una interfaz web usable en el teléfono móvil + - Animar a los usuarios no expertos a profundizar en su comprensión técnica de las redes mesh + - Escalabilidad + - Segmentación de red + - Roaming en capa 2 dentro de ciertas áreas + - Selección inteligente de gateway con redundancia y posibilidad de elección por el usuario + - Compatibilidad con muchos escenarios distintos + - Una única imagen de firmware para todos los routers de tu red + +## Lo básico + +La arquitectura de red diseñada para LibreMesh se basa en dos capas: + +### Capa 2 en la nube +::: info La capa 2 en la nube usa el protocolo de enrutamiento dinámico [BATMAN-adv](https://en.wikipedia.org/wiki/B.A.T.M.A.N.). + +![batmanadv](/batmanadv_logo.svg){width=250 height=200} + +*** +B.A.T.M.A.N. Advanced es un protocolo de enrutamiento mesh que se ejecuta en el espacio del kernel. +Aunque la topología de la red esté formada por múltiples nodos y múltiples saltos, B.A.T.M.A.N. Advanced la abstrae como un único dominio de difusión en capa 2. +De este modo, desde el punto de vista del usuario, toda la mesh se ve como una sola LAN. +Esta arquitectura es robusta para el roaming, por lo que las conexiones TCP y UDP no se pierden al moverse entre puntos de acceso. +*** +::: + +### Capa 3 de red +::: info Toda la capa 3 de la red usa por defecto el protocolo de enrutamiento [Babel](https://en.wikipedia.org/wiki/Babel_(protocol)). +![Babel](/babel_logo.svg){width=200 height=200} + +*** +Babel es un protocolo de enrutamiento robusto y eficiente tanto para redes mesh inalámbricas como cableadas +*** +::: + +### Mezclando capas + +Por defecto, todos los nodos ejecutan ambos protocolos de enrutamiento (Babel y BATMAN-adv), pero en VLANs distintas.footnote:[LAN virtual aislada, por ejemplo wlan0.13]. + +::: tip NOTA +La VLAN de Babel es siempre la misma, así que todos los nodos conectados a nivel de enlace se ven entre sí. +La VLAN de BATMAN-adv depende del identificador de la nube, que se calcula (por defecto) a partir del hash del SSID del AP. + +La red Babel será una única red para toda la mesh, pero la red BATMAN-adv puede estar separada entre distintas nubes locales. +::: + +![](/network1.png) + +**Esta configuración aísla las nubes de capa 2**. +Por ejemplo, un barrio, un complejo de oficinas o una red de hotspots a nivel de calle pueden elegir aislar su LAN del resto de la red. +Al mismo tiempo, podrán alcanzar el resto de los nodos usando la red enrutada de capa 3. + +El roaming estará disponible dentro de la nube, por lo que las sesiones TCP, el streaming de vídeo o incluso una llamada SIP se pueden mantener mientras te mueves. +Por otro lado, gracias a la segmentación de capa 3, los problemas habituales de una red puenteada en capa 2 —como tormentas de broadcast o pesadillas de DHCP— +no afectarán al correcto funcionamiento de la red. + +::: tip +Todo es automático y transparente para el usuario final. +::: + +![](/network2.png) + + +## Los detalles + +Los puntos de acceso WiFi de la misma nube comparten parámetros comunes: + +* El SSID (el nombre identificador del AP WiFi) +* Direcciones IPv4 e IPv6 anycast especiales.footnote:[IP compartida por varios dispositivos en la red] +* Una dirección MAC anycast especial +* El servidor DHCP/RA que proporciona IPs válidas desde la nube a los clientes. + +Así, un cliente conectado a un AP puede moverse por la mesh sin necesidad de renovar su configuración IP. +Incluso la capa MAC será la misma desde su punto de vista. + +![](/network3.png) + +::: tip +El archivo de concesiones DHCP se comparte entre la nube para evitar colisiones usando +[shared-state](https://github.com/libremesh/shared-state-async) + +Mientras todos los nodos compartan el mismo MAC/IP anycast, desde el punto de vista del cliente es totalmente transparente. +El gateway siempre es el mismo aunque el nodo mesh (al que está conectado) cambie. +::: + +![](/network4.png) + +Cuando un cliente quiere salir de la LAN (nube) para llegar a Internet o a cualquier otra red, +enviará los paquetes a la dirección anycast especial del gateway. El nodo al que el cliente +está físicamente conectado se encargará de esto. + +::: tip +Una regla de nftables en el bridge LAN/AP evita que los paquetes enviados a la dirección anycast +se propaguen por la nube. +El nodo mesh al que está asociado el cliente recibe el paquete, pero los demás no. +::: + +![](/network5.png) + +El paquete se enruta a través de la red Babel de capa 3 hasta el gateway de Internet más cercano. +Puede ser un nodo de la misma nube o cualquier otro de una nube lejana. + +![](/network6.png) + + +## Diagramas + +- Listado de diagramas de todas las interfaces de red creadas en LibreMesh ejecutándose en un router **OpenWrt One**. +[![diagram_libremesh-interfaces-openwrt-one](/diagram_libremesh-interfaces-openwrt-one.png)](/diagrams/libremesh-interfaces-openwrt-one) diff --git a/docs/pt-BR/features.md b/docs/pt-BR/features.md new file mode 100644 index 00000000..db0212b8 --- /dev/null +++ b/docs/pt-BR/features.md @@ -0,0 +1,13 @@ +--- +outline: deep +--- + +# Recursos + +## Modularidade + +Com uma única base de código, o LibreMesh oferece uma escolha de `padrões razoáveis` para os protocolos mesh mais comuns: `Babel`, `B.A.T.M.A.N. advanced`, `BMX7`, `OLSR`. + +Ele pode ser usado para se juntar a uma rede mesh existente com configuração mínima. + +Pode ser usado como ambiente de testes para configurações de diferentes protocolos. diff --git a/docs/pt-BR/getting-started.md b/docs/pt-BR/getting-started.md new file mode 100644 index 00000000..13008248 --- /dev/null +++ b/docs/pt-BR/getting-started.md @@ -0,0 +1,79 @@ +# Primeiros passos + +## Instalação + +### Requisitos +Consulte a [Tabela de hardware (Table of Hardware)](https://toh.openwrt.org) para ver se o seu dispositivo é compatível com o OpenWrt. + +::: tip NOTA +Recomenda-se que o roteador tenha **pelo menos**: + - 16 MB de memória flash e 128 MB de RAM. + - 1 rádio funcionando em 2,4 GHz e 1 em 5 GHz + +É possível instalar o LibreMesh em roteadores com 8 MB de flash e 64 MB de RAM +Leia o [`8/64 warning`](https://openwrt.org/supported_devices/864_warning) do OpenWrt e consulte a página [seleção de pacotes (em)](/guide/packages-selection) para personalizar a compilação. +::: + +::: warning ATENÇÃO +Certifique-se de ter lido a página da wiki do OpenWrt sobre o seu dispositivo. +Leia as instruções de instalação e verifique se você tem o hardware necessário — se for o caso — para instalar o firmware, como um [adaptador USB-UART/Serial](https://openwrt.org/docs/guide-user/installation/generic.flashing.serial) e/ou um +[adaptador USB-JTAG](https://openwrt.org/docs/techref/hardware/port.jtag) +::: + +### Baixar o firmware +--- + +**Firmware Selector** +O Firmware Selector solicita uma compilação de firmware através de uma instância do [`ASU`](https://github.com/openwrt/asu) (ImageBuilder online). + +https://firmware-selector.libremesh.org + +--- + +**Versões pré-compiladas** +Arquivo de releases antigas com firmwares pré-compilados via Buildroot. + +https://firmware-libremesh.antennine.org + +--- + +**Compile o LibreMesh no seu host** +Consulte a página [`Compilar o LibreMesh` (em)](/build/) para ver as instruções de como compilar o LibreMesh no seu host. + +--- + +### Instalar o firmware +Instale o firmware no seu dispositivo seguindo o método de instalação indicado na [wiki do OpenWrt](https://openwrt.org) +ou, se não estiver lá, procure as instruções na mensagem de **`git-commit`** deixada por quem adicionou o suporte para aquele modelo de dispositivo. Veja a [Tabela de hardware](https://toh.openwrt.org). + +::: tip NOTA +Se o seu dispositivo estiver rodando o firmware de fábrica, é recomendado **instalar o OpenWrt primeiro**: +::: + +1. Baixe o firmware `stable` mais recente para o seu dispositivo no [`OpenWrt Firmware Selector`](https://firmware-selector.openwrt.org). + Use a imagem `factory` para a primeira instalação. Consulte [Factory Install: First Time Installation (em)](https://openwrt.org/docs/guide-quick-start/factory_installation): +2. Verifique se o dispositivo com OpenWrt inicializa e funciona corretamente. + Atenção: por padrão, o OpenWrt não liga o Wi-Fi. + Ligue-o pelo `LuCI` no menu `Network` / `Wireless`. +3. Atualize para o LibreMesh usando uma imagem `squashfs-sysupgrade.bin`: + - Envie o firmware pela interface web `LuCI` no menu `System` / `Backup / Flash Firmware`. + - Ou instale via SSH usando o comando `sysupgrade -n firmware.bin`. + + +## Conectando ao roteador +O roteador pode ser acessado pela web em http://thisnode.info. +Veja a página [Conectando ao roteador (em)](/guide/connecting) para opções detalhadas e solução de problemas. + + +## Configuração +O LibreMesh vem com um [sabor padrão (em)](/reference/flavors) que funciona out of the box, sem precisar de configuração manual. + +Veja a página de [configuração (em)](/reference/configuration) para opções detalhadas. + + +## Manutenção +Instale versões estáveis mais recentes do OpenWrt para manter o dispositivo atualizado: +- Inscreva-se na newsletter [`OpenWrt Announce`](https://lists.openwrt.org/mailman/listinfo/openwrt-announce). +- Ou siga o [`OpenWrt Announcement-Bot`](https://social.tchncs.de/@openwrt) no Mastodon. + +Veja a página [Atualizar (em)](/guide/upgrade) para operações recomendadas com o LibreMesh. diff --git a/docs/pt-BR/guide/connecting.md b/docs/pt-BR/guide/connecting.md new file mode 100644 index 00000000..8134caa0 --- /dev/null +++ b/docs/pt-BR/guide/connecting.md @@ -0,0 +1,93 @@ +# Conectando ao roteador + +## pelo navegador web +No FQDN do anygw padrão http://thisnode.info. + +Esses endereços também estão disponíveis: + - http://10.13.0.1 - anygw padrão ipv4 + - http://10.13.x.x - endereço ipv4 padrão do nó na LAN `br-lan` + - http://meuno.info - outro FQDN do anygw + - http://minodo.info - outro FQDN do anygw + - http://\ - `hostname` do dispositivo + - http://\.thisnode.info - FQDN do dispositivo + - http://\[fd0d:fe46:8ce8::1\] - anygw padrão ipv6 + - http://\[fd0d:fe46:8ce8::x\:xx\] - endereço ipv6 padrão do nó na LAN `br-lan` + +![lime-app](/lime-app.png) + +## por SSH + +Veja [SSH no OpenWrt (em)](https://openwrt.org/docs/guide-quick-start/sshadministration) + +Os seguintes aliases de bash são recomendados: +- `+ssh-rsa` - (opcional) Necessário se o nó roda uma versão do OpenWrt anterior à branch 23.05 +- `ussh` - um alias para: + - Ignorar arquivos de hosts conhecidos, útil ao se conectar ao endereço do Anygw `10.13.0.1` ou ao FQDN do anygw `thisnode.info` + - Desativar `StrictHostKeyChecking`, útil para reconectar após atualizar para uma versão mais recente do OpenWrt sem "manter configurações". + +```sh +cat << EOF >> ~/.bashrc +alias ssh="ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa" +alias ussh="ssh -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" +EOF +source ~/.bashrc +``` + +Conecte-se ao dispositivo +```sh +ussh root@thisnode.info +``` + +Uma vez estabelecida a conexão, os banners padrão serão impressos no console: +``` +BusyBox v1.37.0 (2026-03-05 17:27:01 UTC) built-in shell (ash) + + _______ ________ __ + | |.-----.-----.-----.| | | |.----.| |_ + | - || _ | -__| || | | || _|| _| + |_______|| __|_____|__|__||________||__| |____| + |__| W I R E L E S S F R E E D O M + ----------------------------------------------------- + OpenWrt 25.12.0, r32713-f919e7899d Dave's Guitar + ----------------------------------------------------- + + === WARNING! ===================================== + There is no root password defined on this device! + Use the "passwd" command to set up a new password + in order to prevent unauthorized SSH logins. + -------------------------------------------------- + + ___ __ __ _______ __ + | |_|__| |--.----.-----.| | |-----.-----| |--. + | | | _ | _| -__|| | -__|__ --| | + |_____|__|_____|__| |_____||__|_|__|_____|_____|__|__| + + ------------------------------------------------------ + LiMe master development (master rev. 7929208 20260304_1752) + ------------------------------------------------------ + https://libremesh.org + ------------------------------------------------------ + + === System Notes ================================================= + + = edit via http://thisnode.info/app/#/notes or /etc/banner.notes = + + + OpenWrt recently switched to the "apk" package manager! + + OPKG Command APK Equivalent Description + ------------------------------------------------------------------ + opkg install apk add Install a package + opkg remove apk del Remove a package + opkg upgrade apk upgrade Upgrade all packages + opkg files apk info -L List package contents + opkg list-installed apk info List installed packages + opkg update apk update Update package lists + opkg search apk search Search for packages + ------------------------------------------------------------------ + +For more information visit: +https://openwrt.org/docs/guide-user/additional-software/opkg-to-apk-cheatsheet + +root@openwrt:~# +``` diff --git a/docs/pt-BR/what-is-libremesh.md b/docs/pt-BR/what-is-libremesh.md new file mode 100644 index 00000000..0a80d4a3 --- /dev/null +++ b/docs/pt-BR/what-is-libremesh.md @@ -0,0 +1,117 @@ +--- +outline: deep +--- + +# O que é o LibreMesh? + +## Objetivos a alcançar + + - Permitir que usuários não especialistas montem uma rede mesh e façam a configuração e o gerenciamento básicos através de uma interface web usável no celular + - Incentivar usuários não especializados a se apropriarem da tecnologia, aprofundando seu entendimento técnico das redes mesh + - Escalabilidade + - Segmentação de rede + - Roaming em camada 2 dentro de certas áreas + - Seleção inteligente de gateway com redundância e possibilidade de escolha pelo usuário + - Compatibilidade com diversos cenários + - Uma única imagem de firmware para todos os roteadores da sua rede + +## O básico + +A arquitetura de rede projetada para o LibreMesh se baseia em duas camadas: + +### Camada 2 em nuvem +::: info A camada 2 em nuvem usa o protocolo de roteamento dinâmico [BATMAN-adv](https://en.wikipedia.org/wiki/B.A.T.M.A.N.). + +![batmanadv](/batmanadv_logo.svg){width=250 height=200} + +*** +B.A.T.M.A.N. Advanced é um protocolo de roteamento mesh que roda no espaço do kernel. +Mesmo que a topologia da rede seja formada por múltiplos nós e múltiplos saltos, o B.A.T.M.A.N. Advanced a abstrai como um único domínio de broadcast em camada 2. +Assim, do ponto de vista do usuário, toda a mesh se parece com uma única LAN. +Essa arquitetura é robusta para fins de roaming, de modo que conexões TCP e UDP não se perdem ao se mover e trocar de ponto de acesso. +*** +::: + +### Camada 3 de rede +::: info Toda a camada 3 da rede usa por padrão o protocolo de roteamento [Babel](https://en.wikipedia.org/wiki/Babel_(protocol)). +![Babel](/babel_logo.svg){width=200 height=200} + +*** +Babel é um protocolo de roteamento robusto e eficiente tanto para redes mesh sem fio quanto para redes cabeadas +*** +::: + +### Misturando as camadas + +Por padrão, todos os nós rodam ambos os protocolos de roteamento (Babel e BATMAN-adv), mas em VLANs diferentes.footnote:[LAN virtual isolada, por exemplo wlan0.13]. + +::: tip NOTA +A VLAN do Babel é sempre a mesma, então todos os nós conectados no nível de enlace se enxergam. +A VLAN do BATMAN-adv depende do identificador da nuvem, que é calculado (por padrão) a partir do hash do SSID do AP. + +A rede Babel será uma única rede para toda a mesh, mas a rede BATMAN-adv pode ser separada entre diferentes nuvens locais. +::: + +![](/network1.png) + +**Essa configuração isola as nuvens de camada 2**. +Por exemplo, um bairro, um complexo de empresas ou uma rede de hotspots ao nível de rua podem optar por isolar sua LAN do restante da rede. +Ao mesmo tempo, conseguirão alcançar os demais nós usando a rede roteada de camada 3. + +O roaming estará disponível dentro da nuvem, de modo que sessões TCP, streaming de vídeo ou até mesmo uma chamada SIP podem ser mantidas enquanto você se move. +Por outro lado, graças à segmentação de camada 3, os problemas comuns encontrados em uma rede bridgeada em camada 2 —como broadcast storms ou pesadelos de DHCP— +não atrapalharão o funcionamento correto da rede. + +::: tip +Tudo é automático e transparente para o usuário final. +::: + +![](/network2.png) + + +## Os detalhes + +Os pontos de acesso WiFi da mesma nuvem compartilham parâmetros comuns: + +* O SSID (o nome de identificação do AP WiFi) +* Endereços IPv4 e IPv6 anycast especiais.footnote:[IP compartilhado por vários dispositivos na rede] +* Um endereço MAC anycast especial +* O servidor DHCP/RA que fornece IPs válidos da nuvem para os clientes. + +Assim, um cliente conectado a um AP pode se mover pela mesh sem precisar renovar sua configuração IP. +Até a camada MAC será a mesma do ponto de vista dele. + +![](/network3.png) + +::: tip +O arquivo de concessões DHCP é compartilhado entre a nuvem para evitar colisões usando +[shared-state](https://github.com/libremesh/shared-state-async) + +Desde que todos os nós compartilhem o mesmo MAC/IP anycast, do ponto de vista do cliente é totalmente transparente. +O gateway é sempre o mesmo, mesmo que o nó mesh (ao qual o cliente está conectado) mude. +::: + +![](/network4.png) + +Quando um cliente quer sair da LAN (nuvem) para alcançar a Internet ou qualquer outra rede, +ele enviará os pacotes para o endereço anycast especial do gateway. O nó ao qual o cliente +está fisicamente conectado se encarregará disso. + +::: tip +Uma regra de nftables na bridge LAN/AP evita que os pacotes enviados ao endereço anycast +se propaguem pela nuvem. +O nó mesh ao qual o cliente está associado recebe o pacote, mas os outros não. +::: + +![](/network5.png) + +O pacote é roteado pela rede Babel de camada 3 até o gateway de Internet mais próximo. +Pode ser um nó da mesma nuvem ou qualquer outro de uma nuvem distante. + +![](/network6.png) + + +## Diagramas + +- Listagem de diagramas de todas as interfaces de rede criadas no LibreMesh rodando em um roteador **OpenWrt One**. +[![diagram_libremesh-interfaces-openwrt-one](/diagram_libremesh-interfaces-openwrt-one.png)](/diagrams/libremesh-interfaces-openwrt-one) From 24d52142543383031f9613674d5d33c5a661c5ff Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 20:09:01 -0300 Subject: [PATCH 02/25] fix(i18n): drop top banner, use tiny floating toast; fix useRoute() error The full-width banner that lived in the layout-top slot was pushing the home page content down and breaking the mobile menu. The VitePress built-in language switcher (auto-rendered in the right side of the nav from the locales config) is the tiny menu the user wants, and it already renders consistently on home and doc pages. Replace the banner with a small fixed-position toast in the bottom-right corner so it never touches the nav and never shifts layout. Mounted via the page-bottom slot in theme/index.ts; styled in style.css as a discrete chip with Switch and dismiss buttons. The toast is hidden by default and only appears on first visit when navigator.language matches a known locale. Also fix the runtime error from the previous build: Uncaught Error: useRouter() is called without provider. at useRouter (router.js?v=...:162:15) at useRoute (router.js?v=...:166:12) VitePress's useRoute() must be called once at setup top-level so it can register with the provided router context. Calling it inside onMounted and event handlers produced the missing-provider error. Move the call to the top of diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index ae4ed0a4..65aa2b26 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -13,7 +13,7 @@ export default { return h(DefaultTheme.Layout, null, { // https://vitepress.dev/guide/extending-default-theme#layout-slots 'wide': () => h(LayoutWide), - 'layout-top': () => h(LanguageBanner) + 'page-bottom': () => h(LanguageBanner) }) }, enhanceApp({ app, router, siteData }) { diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index f542ee6e..17d9694b 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -140,56 +140,72 @@ } /** - * Component: Language suggestion banner + * Component: Language suggestion toast + * Floating chip in the bottom-right corner. Out of the nav so it + * never overlaps the menu and never shifts layout on mobile. * -------------------------------------------------------------------------- */ -.lang-banner { - display: flex; - flex-wrap: wrap; +.lang-toast { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 200; + display: inline-flex; align-items: center; - justify-content: center; - gap: 0.75rem 1.25rem; - padding: 0.6rem 1.25rem; - background: var(--vp-c-brand-soft); + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background: var(--vp-c-bg-soft, var(--vp-c-bg)); color: var(--vp-c-text-1); - font-size: 0.9rem; - border-bottom: 1px solid var(--vp-c-divider); - text-align: center; + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); + font-size: 0.85rem; + max-width: calc(100vw - 2rem); } -.lang-banner__msg strong { - font-weight: 600; +.lang-toast__msg { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.lang-banner__actions { - display: inline-flex; - gap: 0.5rem; +.lang-toast__msg strong { + font-weight: 600; } -.lang-banner__btn { +.lang-toast__btn { appearance: none; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg); color: var(--vp-c-text-1); - padding: 0.25rem 0.75rem; - font-size: 0.85rem; + padding: 0.15rem 0.55rem; + font-size: 0.8rem; border-radius: 6px; cursor: pointer; font-family: inherit; + line-height: 1.4; } -.lang-banner__btn:hover { +.lang-toast__btn:hover { border-color: var(--vp-c-brand-1); } -.lang-banner__btn--primary { +.lang-toast__btn--primary { background: var(--vp-c-brand-3); color: var(--vp-c-white); border-color: var(--vp-c-brand-3); } -.lang-banner__btn--primary:hover { +.lang-toast__btn--primary:hover { background: var(--vp-c-brand-2); border-color: var(--vp-c-brand-2); } +@media (max-width: 480px) { + .lang-toast { + right: 0.5rem; + bottom: 0.5rem; + font-size: 0.8rem; + } +} + From 1aa3f9ea96cd39f08452aaa48f789d246b510bd1 Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 20:16:09 -0300 Subject: [PATCH 03/25] fix(i18n): add translated home pages for es and pt-BR Visiting /es/ or /pt-BR/ in preview returned PAGE NOT FOUND because VitePress i18n requires a per-locale index.md at the root of each locale directory. The build emitted the four inner pages for each locale but skipped the locale root, so the language switcher in the nav pointed at a 404 on the home view. Add docs/es/index.md and docs/pt-BR/index.md as translated versions of docs/index.md, mirroring its layout: home hero, features, organizations table, community mesh networks table. Frontmatter details text is rephrased to avoid trailing colons inside a single-line YAML value, which broke the parser on the first attempt. --- docs/es/index.md | 78 +++++++++++++++++++++++++++++++++++++++++++++ docs/pt-BR/index.md | 78 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 docs/es/index.md create mode 100644 docs/pt-BR/index.md diff --git a/docs/es/index.md b/docs/es/index.md new file mode 100644 index 00000000..4a384841 --- /dev/null +++ b/docs/es/index.md @@ -0,0 +1,78 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "LibreMesh" + text: "Un framework modular para crear firmwares basados en OpenWrt para nodos de mesh inalámbricos" + tagline: Hacer posible las redes libres + image: + src: ./lime.svg + actions: + - theme: brand + text: ¿Qué es LibreMesh? + link: /es/what-is-libremesh + - theme: alt + text: Primeros pasos + link: /es/getting-started + - theme: alt + text: Firmware Selector + link: https://firmware-selector.libremesh.org + +features: + - icon: + src: /network_topology_mesh_icon.png + title: Redes mesh + details: + Las redes mesh son redes en las que todos los participantes (nodos) pueden enrutar el tráfico de otros participantes. + No hay puntos centrales y la topología física puede ser completamente aleatoria. + Por lo general, las redes mesh son descentralizadas, organizadas desde abajo hacia arriba, desplegadas y mantenidas por las personas que las usan. + Creemos que es la única manera de conseguir una red realmente libre, fuera del control de gobiernos y empresas globales. + - icon: + src: /gnu_logo.png + title: Redes libres, sociedad libre + details: Entendemos que una red libre, como red de telecomunicaciones, debe cumplir estos tres puntos. + Es abierta, así que cualquiera puede conectarse si es físicamente posible. + Es neutral, por lo que no hay preferencias por el tipo, origen o destino de los datos. + Es libre como la libertad (libre significa "free as in free speech" en inglés). + Todo lo que desarrollamos es software libre para una sociedad libre, de modo que cualquiera pueda usarlo, copiarlo, modificarlo y distribuirlo bajo la licencia AGPL. + + - icon: + src: /openwrt_logo_icon.png + title: Firmware y dispositivos embebidos + details: Un dispositivo embebido es una computadora pequeña. Normalmente, al sistema operativo que corre en esas computadoras pequeñas se le llama firmware. + Nuestra forma de desplegar redes mesh libres es instalando nuestro propio firmware en los dispositivos (normalmente routers WiFi). + Nuestro sistema se basa en el proyecto OpenWrt, que a su vez se basa en el conocido sistema operativo Linux. +--- + +## Organizaciones que apoyan a LibreMesh: + +| | | | | +| -------------| ----------------- | -------------------------- | ------------------------------------- | +| AlterMundi | Argentina | https://altermundi.net | ![AlterMundi](/altermundi_logo.png) | +| Coolab | Brasil | https://wiki.coolab.org | ![Coolab](/coolab_logo.png) | +| FreiFunk | Alemania | https://freifunk.net | ![FreiFunk](/freifunk_logo.png) | +| FunkFeuer | Austria | https://funkfeuer.at | ![FunkFeuer](/funkfeuer_logo.png) | +| Guifi | Península Ibérica | https://guifi.net | ![Guifi](/guifi_logo.png) | +| IBEBrasil | Brasil | https://ibebrasil.org.br | ![IBEBrasil](/ibebrasil_logo.png) | +| LibreRouter | Global | https://librerouter.org | ![LibreRouter](/librerouter_logo.png) | +| Ninux | Italia | https://ninux.org | ![Ninux](/ninux_logo.png) | +| NUPEF | Brasil | https://nupef.org.br/ | | +| Wakoma | Global | https://wakoma.co | ![Wakoma](/wakoma_logo.png) | + +## Redes mesh comunitarias que usan LibreMesh: + +| | | | | +| --------------- | ------------------- | ----------------------------------------- | ------------------------- | +| Antennine | Bolonia, Italia | https://antennine.noblogs.org | ![Ninux](/ninux_logo.png) | +| Calafou | Cataluña | https://calafou.org/ | | +| Coolab | Brasil | https://www.coolab.org/ | | +| Janastu CowMesh | Karnataka rural | https://open.janastu.org/projects/cowmesh | | +| NUPEF | Brasil | https://nupef.org.br/ | | + + diff --git a/docs/pt-BR/index.md b/docs/pt-BR/index.md new file mode 100644 index 00000000..90d19cbe --- /dev/null +++ b/docs/pt-BR/index.md @@ -0,0 +1,78 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "LibreMesh" + text: "Um framework modular para criar firmwares baseados em OpenWrt para nós mesh sem fio" + tagline: Tornando possíveis as redes livres + image: + src: ./lime.svg + actions: + - theme: brand + text: O que é o LibreMesh? + link: /pt-BR/what-is-libremesh + - theme: alt + text: Primeiros passos + link: /pt-BR/getting-started + - theme: alt + text: Firmware Selector + link: https://firmware-selector.libremesh.org + +features: + - icon: + src: /network_topology_mesh_icon.png + title: Redes mesh + details: + Redes mesh são redes nas quais todos os participantes (nós) podem rotear o tráfego de outros participantes. + Não há pontos centrais e a topologia física pode ser completamente aleatória. + Geralmente as redes mesh são descentralizadas, organizadas de baixo para cima, implantadas e mantidas pelas pessoas que as utilizam. + Acreditamos que essa é a única forma de alcançar uma rede verdadeiramente livre, fora do controle de governos e grandes empresas. + - icon: + src: /gnu_logo.png + title: Redes livres, sociedade livre + details: Entendemos que uma rede livre, como rede de telecomunicações, deve cumprir estes três pontos. + É aberta, de modo que qualquer pessoa possa se conectar se for fisicamente possível. + É neutra, sem preferências pelo tipo, origem ou destino dos dados. + É livre como em liberdade (libre significa "free as in free speech" em inglês). + Tudo o que desenvolvemos é software livre para uma sociedade livre, para que qualquer pessoa possa usar, copiar, modificar e distribuir sob a licença AGPL. + + - icon: + src: /openwrt_logo_icon.png + title: Firmware e dispositivos embarcados + details: Um dispositivo embarcado é um computador pequeno. Normalmente, o sistema operacional que roda nesses computadores pequenos é chamado de firmware. + Nossa forma de implantar redes mesh livres é instalando nosso próprio firmware nos dispositivos (geralmente roteadores WiFi). + Nosso sistema é baseado no projeto OpenWrt, que por sua vez é baseado no conhecido sistema operacional Linux. +--- + +## Organizações que apoiam o LibreMesh: + +| | | | | +| -------------| ----------------- | -------------------------- | ------------------------------------- | +| AlterMundi | Argentina | https://altermundi.net | ![AlterMundi](/altermundi_logo.png) | +| Coolab | Brasil | https://wiki.coolab.org | ![Coolab](/coolab_logo.png) | +| FreiFunk | Alemanha | https://freifunk.net | ![FreiFunk](/freifunk_logo.png) | +| FunkFeuer | Áustria | https://funkfeuer.at | ![FunkFeuer](/funkfeuer_logo.png) | +| Guifi | Península Ibérica | https://guifi.net | ![Guifi](/guifi_logo.png) | +| IBEBrasil | Brasil | https://ibebrasil.org.br | ![IBEBrasil](/ibebrasil_logo.png) | +| LibreRouter | Global | https://librerouter.org | ![LibreRouter](/librerouter_logo.png) | +| Ninux | Itália | https://ninux.org | ![Ninux](/ninux_logo.png) | +| NUPEF | Brasil | https://nupef.org.br/ | | +| Wakoma | Global | https://wakoma.co | ![Wakoma](/wakoma_logo.png) | + +## Redes mesh comunitárias que usam o LibreMesh: + +| | | | | +| --------------- | ------------------- | ----------------------------------------- | ------------------------- | +| Antennine | Bolonha, Itália | https://antennine.noblogs.org | ![Ninux](/ninux_logo.png) | +| Calafou | Catalunha | https://calafou.org/ | | +| Coolab | Brasil | https://www.coolab.org/ | | +| Janastu CowMesh | Karnataka rural | https://open.janastu.org/projects/cowmesh | | +| NUPEF | Brasil | https://nupef.org.br/ | | + + From 9c50b4fb4c1c601755e5a4e2e3a2128ba56d649f Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 20:20:34 -0300 Subject: [PATCH 04/25] fix(i18n): use relative sidebar items under locale base The localized sidebars had base: '/es/' (and '/pt-BR/') and items with absolute paths like '/es/what-is-libremesh'. VitePress concatenated them, producing broken links of the form '/es/es/what-is-libremesh.html' from any page on the /es/ tree. Make the items relative (drop the '/es/' / '/pt-BR/' prefix) so base prepends correctly. The English sidebar already used this pattern with base: '/' and absolute items. --- docs/.vitepress/config.mts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index fc538950..aa306b0b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -232,16 +232,16 @@ function sidebarGuideEs(): DefaultTheme.SidebarItem[] { text: 'Introducción', collapsed: false, items: [ - { text: '¿Qué es LibreMesh?', link: '/es/what-is-libremesh' }, - { text: 'Primeros pasos', link: '/es/getting-started' }, - { text: 'Características', link: '/es/features' }, + { text: '¿Qué es LibreMesh?', link: 'what-is-libremesh' }, + { text: 'Primeros pasos', link: 'getting-started' }, + { text: 'Características', link: 'features' }, ] }, { text: 'Guía de uso', collapsed: false, items: [ - { text: 'Conectarse al router', link: '/es/guide/connecting' }, + { text: 'Conectarse al router', link: 'guide/connecting' }, ] }, ] @@ -253,16 +253,16 @@ function sidebarGuidePtBr(): DefaultTheme.SidebarItem[] { text: 'Introdução', collapsed: false, items: [ - { text: 'O que é o LibreMesh?', link: '/pt-BR/what-is-libremesh' }, - { text: 'Primeiros passos', link: '/pt-BR/getting-started' }, - { text: 'Recursos', link: '/pt-BR/features' }, + { text: 'O que é o LibreMesh?', link: 'what-is-libremesh' }, + { text: 'Primeiros passos', link: 'getting-started' }, + { text: 'Recursos', link: 'features' }, ] }, { text: 'Guia de uso', collapsed: false, items: [ - { text: 'Conectando ao roteador', link: '/pt-BR/guide/connecting' }, + { text: 'Conectando ao roteador', link: 'guide/connecting' }, ] }, ] From cb5f00d728a2cae9c67a1daddfb6dfdb9c136919 Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 20:26:01 -0300 Subject: [PATCH 05/25] fix(i18n): use absolute /lime.svg path in translated home pages The hero image was referenced as './lime.svg' which VitePress resolved relative to the page URL: '/es/lime.svg' and '/pt-BR/lime.svg'. The asset lives at the site root (docs/public/lime.svg), so those paths 404'd. Switch to the absolute '/lime.svg' so the image resolves the same from every locale. Same as the English home page. --- docs/es/index.md | 2 +- docs/pt-BR/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/es/index.md b/docs/es/index.md index 4a384841..a22b5266 100644 --- a/docs/es/index.md +++ b/docs/es/index.md @@ -7,7 +7,7 @@ hero: text: "Un framework modular para crear firmwares basados en OpenWrt para nodos de mesh inalámbricos" tagline: Hacer posible las redes libres image: - src: ./lime.svg + src: /lime.svg actions: - theme: brand text: ¿Qué es LibreMesh? diff --git a/docs/pt-BR/index.md b/docs/pt-BR/index.md index 90d19cbe..19fcbc16 100644 --- a/docs/pt-BR/index.md +++ b/docs/pt-BR/index.md @@ -7,7 +7,7 @@ hero: text: "Um framework modular para criar firmwares baseados em OpenWrt para nós mesh sem fio" tagline: Tornando possíveis as redes livres image: - src: ./lime.svg + src: /lime.svg actions: - theme: brand text: O que é o LibreMesh? From 3d7c19833da3686ba00abdbd1bfcd9f80b48c4ed Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 20:33:00 -0300 Subject: [PATCH 06/25] fix(i18n): smart redirect, link-out nav, drop reference sidebar from locales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final review pass. Four real issues found and fixed: 1. Localized nav 404'd. navEs() and navPtBr() pointed at /es/guide/packages-selection, /es/reference/configuration, /es/news/..., /es/changelog — none of which exist. Switch to the link-out convention: localized labels, English targets. Same structure as the English nav, just translated. 2. Reference sidebar removed from the es and pt-BR locales. The reference pages aren't translated, and sidebarReferenceEn() would have rendered English labels inside a Spanish sidebar. With the nav now pointing at English, the sidebar is not needed in those locales. 3. LanguageBanner used to dump the user on the locale home when they accepted, even if the page they were reading had a translated counterpart. Added a translatedPaths map for both locales so the toast takes them to the matching localized page (e.g. /guide/connecting -> /es/guide/connecting). Added a per-locale action label ('Cambiar' / 'Trocar') and a per-locale hint string so the toast reads in the user's language, not in English. 4. TRANSLATING.md said 'banner' for what is now a 'toast', and was missing a 'Submitting a translation PR' section. Updated both, and added a note that new translatedPaths entries should be added when a page is translated in both languages. --- docs/.vitepress/config.mts | 26 ++++++++---------- docs/.vitepress/theme/LanguageBanner.vue | 35 +++++++++++++++++++----- docs/.vitepress/theme/style.css | 2 +- docs/TRANSLATING.md | 15 ++++++++-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index aa306b0b..193de8d5 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -38,7 +38,6 @@ export default defineConfig({ nav: navEs(), sidebar: { '/es/': { base: '/es/', items: sidebarGuideEs() }, - '/es/reference/': { base: '/es/reference/', items: sidebarReferenceEn() } }, }, }, @@ -50,7 +49,6 @@ export default defineConfig({ nav: navPtBr(), sidebar: { '/pt-BR/': { base: '/pt-BR/', items: sidebarGuidePtBr() }, - '/pt-BR/reference/': { base: '/pt-BR/reference/', items: sidebarReferenceEn() } }, }, }, @@ -104,14 +102,14 @@ function navEn(): DefaultTheme.NavItem[] { function navEs(): DefaultTheme.NavItem[] { return [ - { text: 'Guía', link: '/es/guide/packages-selection' }, - { text: 'Referencia', link: '/es/reference/configuration' }, + { text: 'Guía', link: '/guide/packages-selection' }, + { text: 'Referencia', link: '/reference/configuration' }, { text: 'Noticias', items: [ - { text: 'v2024.1', link: '/es/news/2025-05-04' }, - { text: 'v2020.4', link: '/es/news/2023-10-07' }, - { text: 'Artículos recientes', link: '/es/news/' }, - { text: 'Changelog', link: '/es/changelog'}, + { text: 'v2024.1', link: '/news/2025-05-04' }, + { text: 'v2020.4', link: '/news/2023-10-07' }, + { text: 'Artículos recientes', link: '/news/' }, + { text: 'Changelog', link: '/changelog'}, { text: 'Issues', link: 'https://github.com/libremesh/lime-packages/issues'}, ] }, @@ -120,14 +118,14 @@ function navEs(): DefaultTheme.NavItem[] { function navPtBr(): DefaultTheme.NavItem[] { return [ - { text: 'Guia', link: '/pt-BR/guide/packages-selection' }, - { text: 'Referência', link: '/pt-BR/reference/configuration' }, + { text: 'Guia', link: '/guide/packages-selection' }, + { text: 'Referência', link: '/reference/configuration' }, { text: 'Notícias', items: [ - { text: 'v2024.1', link: '/pt-BR/news/2025-05-04' }, - { text: 'v2020.4', link: '/pt-BR/news/2023-10-07' }, - { text: 'Artigos recentes', link: '/pt-BR/news/' }, - { text: 'Changelog', link: '/pt-BR/changelog'}, + { text: 'v2024.1', link: '/news/2025-05-04' }, + { text: 'v2020.4', link: '/news/2023-10-07' }, + { text: 'Artigos recentes', link: '/news/' }, + { text: 'Changelog', link: '/changelog'}, { text: 'Issues', link: 'https://github.com/libremesh/lime-packages/issues'}, ] }, diff --git a/docs/.vitepress/theme/LanguageBanner.vue b/docs/.vitepress/theme/LanguageBanner.vue index 82a3506d..1b36b522 100644 --- a/docs/.vitepress/theme/LanguageBanner.vue +++ b/docs/.vitepress/theme/LanguageBanner.vue @@ -7,8 +7,26 @@ const showToast = ref(false) const targetLocale = ref(null) const messages = { - es: { label: 'Español', path: '/es/' }, - 'pt-BR': { label: 'Português (BR)', path: '/pt-BR/' } + es: { label: 'Español', home: '/es/', hint: 'Esta página también está disponible en Español.', action: 'Cambiar' }, + 'pt-BR': { label: 'Português (BR)', home: '/pt-BR/', hint: 'Esta página também está disponível em Português (BR).', action: 'Trocar' } +} + +// English pages that have a translated counterpart. When the user +// accepts the toast from one of these, we send them to the matching +// localized page rather than the locale home. +const translatedPaths = { + es: { + '/what-is-libremesh': '/es/what-is-libremesh', + '/getting-started': '/es/getting-started', + '/features': '/es/features', + '/guide/connecting': '/es/guide/connecting', + }, + 'pt-BR': { + '/what-is-libremesh': '/pt-BR/what-is-libremesh', + '/getting-started': '/pt-BR/getting-started', + '/features': '/pt-BR/features', + '/guide/connecting': '/pt-BR/guide/connecting', + } } function pickLocale(navLang) { @@ -19,6 +37,11 @@ function pickLocale(navLang) { return null } +function localizedPath(locale, currentPath) { + const clean = currentPath.replace(/\.html$/, '').replace(/\/$/, '') || '/' + return translatedPaths[locale][clean] || messages[locale].home +} + const route = useRoute() onMounted(() => { @@ -41,7 +64,7 @@ function accept() { const locale = targetLocale.value if (!locale) return try { localStorage.setItem(STORAGE_KEY, locale) } catch (e) {} - window.location.href = messages[locale].path + window.location.href = localizedPath(locale, route.path) } function dismiss() { @@ -52,10 +75,8 @@ function dismiss() { diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 17d9694b..80ff2b66 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -154,7 +154,7 @@ align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; - background: var(--vp-c-bg-soft, var(--vp-c-bg)); + background: var(--vp-c-bg); color: var(--vp-c-text-1); border: 1px solid var(--vp-c-divider); border-radius: 8px; diff --git a/docs/TRANSLATING.md b/docs/TRANSLATING.md index af696bb9..f64ae154 100644 --- a/docs/TRANSLATING.md +++ b/docs/TRANSLATING.md @@ -48,13 +48,24 @@ Do not invent `/es/...` paths for pages that don't exist in Spanish yet — they 3. Add the language code to the detection list in `docs/.vitepress/theme/LanguageBanner.vue` (`pickLocale` and `messages`) so first-visit visitors see the suggestion. 4. Add the new locale to this guide's "What to keep in English" list if it introduces new untranslated brand terms. -## First-visit language banner +## First-visit language toast -`LanguageBanner.vue` reads `navigator.language` and, on the first visit (no `localStorage` entry), shows a non-intrusive banner suggesting the localized site. The user choice is stored under `libremesh-lang-pref`: +`LanguageBanner.vue` reads `navigator.language` and, on the first visit (no `localStorage` entry), shows a non-intrusive toast in the bottom-right corner suggesting the localized site. It is mounted via the `page-bottom` slot in `theme/index.ts` and uses `position: fixed`, so it never touches the nav and never shifts layout on mobile. + +The user choice is stored under `libremesh-lang-pref`: - `dismissed` — they declined; do not show again. - `es` / `pt-BR` — they accepted; the stored value is informational only (the actual locale is determined by the URL the user navigates to via the language switcher in the nav). +If the user accepts the toast from a page that has a translated counterpart (e.g. they are on `/guide/connecting` and accept the Spanish toast), they land on the matching `/es/guide/connecting` — not the locale home. The mapping lives in `translatedPaths` inside the component; add new entries there when a new page gets translated in both languages. + +## Submitting a translation PR + +1. Translate the file (or create a new one) under the locale directory. +2. Run `pnpm install && pnpm build` locally to make sure nothing breaks. The build also catches broken links to pages that don't exist yet — see "What about pages that aren't translated yet?" above. +3. Open a PR against `main`. A native speaker should review before merge. +4. If you translated a new English page (so the page now exists in both English and your language), add an entry to the `translatedPaths` map in `LanguageBanner.vue` so the toast can take the reader directly to the translated version. + ## Reviewing a translation PR A native speaker should review every translation PR before merge. Things to check: From d815f0afcedc0720426609b110c5731552f3ef79 Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 20:48:05 -0300 Subject: [PATCH 07/25] fix(i18n): per-locale description, navigator.languages, localized dismiss label Second review pass. Three improvements: 1. Add a 'description' field to each locale in config.mts. The top-level 'description' was being used as on every page in every locale, so search results for es/pt-BR pages showed the English description. Override per locale. 2. LanguageBanner.pickLocale now accepts navigator.languages (an array) in addition to navigator.language (a single string), so a user with navigator.languages = ['fr-FR', 'es-ES', 'en-US'] still gets the Spanish toast offered (es is the first match). The function falls back to the old single-value behavior if navigator.languages is missing. 3. The toast dismiss button had a hardcoded aria-label='Dismiss' in English. Bind it to the per-locale messages object so the screen reader label matches the toast's language (Cerrar / Fechar). Also: in navEn() change 'changelog' (relative) to '/changelog' (absolute) for consistency with the localized navs, which all use absolute paths after the link-out fix. --- docs/.vitepress/config.mts | 5 ++++- docs/.vitepress/theme/LanguageBanner.vue | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 193de8d5..06cfc6d6 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -22,6 +22,7 @@ export default defineConfig({ root: { label: 'English', lang: 'en', + description: 'A modular framework for creating OpenWrt-based firmwares for wireless mesh nodes', themeConfig: { nav: navEn(), sidebar: { @@ -34,6 +35,7 @@ export default defineConfig({ label: 'Español', lang: 'es', link: '/es/', + description: 'Un framework modular para crear firmwares basados en OpenWrt para nodos de mesh inalámbricos', themeConfig: { nav: navEs(), sidebar: { @@ -45,6 +47,7 @@ export default defineConfig({ label: 'Português (BR)', lang: 'pt-BR', link: '/pt-BR/', + description: 'Um framework modular para criar firmwares baseados em OpenWrt para nós mesh sem fio', themeConfig: { nav: navPtBr(), sidebar: { @@ -93,7 +96,7 @@ function navEn(): DefaultTheme.NavItem[] { { text: 'v2024.1', link: '/news/2025-05-04' }, { text: 'v2020.4', link: '/news/2023-10-07' }, { text: 'Latest Articles', link: '/news/' }, - { text: 'Changelog', link: 'changelog'}, + { text: 'Changelog', link: '/changelog'}, { text: 'Issues', link: 'https://github.com/libremesh/lime-packages/issues'}, ] }, diff --git a/docs/.vitepress/theme/LanguageBanner.vue b/docs/.vitepress/theme/LanguageBanner.vue index 1b36b522..49e0c671 100644 --- a/docs/.vitepress/theme/LanguageBanner.vue +++ b/docs/.vitepress/theme/LanguageBanner.vue @@ -7,8 +7,8 @@ const showToast = ref(false) const targetLocale = ref(null) const messages = { - es: { label: 'Español', home: '/es/', hint: 'Esta página también está disponible en Español.', action: 'Cambiar' }, - 'pt-BR': { label: 'Português (BR)', home: '/pt-BR/', hint: 'Esta página também está disponível em Português (BR).', action: 'Trocar' } + es: { label: 'Español', home: '/es/', hint: 'Esta página también está disponible en Español.', action: 'Cambiar', dismiss: 'Cerrar' }, + 'pt-BR': { label: 'Português (BR)', home: '/pt-BR/', hint: 'Esta página também está disponível em Português (BR).', action: 'Trocar', dismiss: 'Fechar' } } // English pages that have a translated counterpart. When the user @@ -29,11 +29,15 @@ const translatedPaths = { } } -function pickLocale(navLang) { - if (!navLang) return null - const lower = navLang.toLowerCase() - if (lower.startsWith('pt')) return 'pt-BR' - if (lower.startsWith('es')) return 'es' +function pickLocale(navLangs) { + if (!navLangs) return null + const list = Array.isArray(navLangs) ? navLangs : [navLangs] + for (const lang of list) { + if (!lang) continue + const lower = String(lang).toLowerCase() + if (lower.startsWith('pt')) return 'pt-BR' + if (lower.startsWith('es')) return 'es' + } return null } @@ -53,7 +57,7 @@ onMounted(() => { const path = route.path if (path.startsWith('/es') || path.startsWith('/pt-BR')) return - const locale = pickLocale(navigator.language) + const locale = pickLocale(navigator.languages || navigator.language) if (!locale) return targetLocale.value = locale @@ -77,6 +81,6 @@ function dismiss() {
{{ messages[targetLocale].hint }} - +
From 3e7e0d6711455082772fe419cb81717e4b8a0e95 Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 21:07:16 -0300 Subject: [PATCH 08/25] fix(i18n): replace built-in lang switcher with custom one that respects translatedPaths Codex (gpt-5.1-codex-max) flagged a P2 in the previous review pass: VitePress's built-in VPNavBarTranslations auto-generates links to // for every locale, which 404s for the many English pages that have no translated counterpart. Example: on /guide/packages-selection the switcher offered /es/guide/packages-selection.html and /pt-BR/guide/packages-selection.html, neither of which exist. Fix: 1. Extract the path map and helpers to a new i18n.ts module so the toast and the switcher use the same target resolution and cannot drift apart. 2. Add LanguageSwitcher.vue: a small dropdown in the nav, mounted via the nav-bar-content-after slot, that resolves the right target for each locale (translated counterpart if one exists, locale home otherwise). The current locale is shown highlighted and a 'home' hint is appended to items that fall back. 3. Hide VitePress's built-in switcher via CSS as a defensive fallback, and remove its DOM element from LanguageSwitcher.vue's onMounted so its dead hrefs never end up in the served HTML (would otherwise be crawled by search engines and treated as 404s). 4. Refactor LanguageBanner.vue to import the same i18n.ts helpers. --- docs/.vitepress/theme/LanguageBanner.vue | 43 +---------- docs/.vitepress/theme/index.ts | 4 +- docs/.vitepress/theme/style.css | 92 ++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 42 deletions(-) diff --git a/docs/.vitepress/theme/LanguageBanner.vue b/docs/.vitepress/theme/LanguageBanner.vue index 49e0c671..919bde1c 100644 --- a/docs/.vitepress/theme/LanguageBanner.vue +++ b/docs/.vitepress/theme/LanguageBanner.vue @@ -1,51 +1,12 @@ + + diff --git a/docs/.vitepress/theme/i18n.ts b/docs/.vitepress/theme/i18n.ts new file mode 100644 index 00000000..3083afbf --- /dev/null +++ b/docs/.vitepress/theme/i18n.ts @@ -0,0 +1,64 @@ +// Shared between LanguageBanner.vue (first-visit toast) and +// LanguageSwitcher.vue (nav dropdown). When a new page is +// translated in both English and one of the locales, add an entry +// here so both UIs take the user to the matching page. + +export const LOCALE_PREFIXES = ['/es', '/pt-BR'] + +export const messages = { + en: { label: 'English', short: 'EN', home: '/' }, + es: { label: 'Español', short: 'ES', home: '/es/' }, + 'pt-BR': { label: 'Português (BR)', short: 'PT', home: '/pt-BR/' } +} + +export const translatedPaths = { + es: { + '/what-is-libremesh': '/es/what-is-libremesh', + '/getting-started': '/es/getting-started', + '/features': '/es/features', + '/guide/connecting': '/es/guide/connecting', + }, + 'pt-BR': { + '/what-is-libremesh': '/pt-BR/what-is-libremesh', + '/getting-started': '/pt-BR/getting-started', + '/features': '/pt-BR/features', + '/guide/connecting': '/pt-BR/guide/connecting', + } +} + +export function stripLocale(path) { + for (const p of LOCALE_PREFIXES) { + if (path === p || path === p + '/' || path.startsWith(p + '/')) { + return path.slice(p.length) || '/' + } + } + return path +} + +export function cleanPath(path) { + return path.replace(/\.html$/, '').replace(/\/$/, '') || '/' +} + +export function targetFor(locale, currentPath) { + const clean = cleanPath(stripLocale(currentPath)) + if (locale === 'en') return clean + return translatedPaths[locale][clean] || messages[locale].home +} + +export function currentLocaleOf(path) { + if (path.startsWith('/es')) return 'es' + if (path.startsWith('/pt-BR')) return 'pt-BR' + return 'en' +} + +export function pickLocale(navLangs) { + if (!navLangs) return null + const list = Array.isArray(navLangs) ? navLangs : [navLangs] + for (const lang of list) { + if (!lang) continue + const lower = String(lang).toLowerCase() + if (lower.startsWith('pt')) return 'pt-BR' + if (lower.startsWith('es')) return 'es' + } + return null +} From 3dd07f82cf3a34462e03719a1ae588c4d077f2eb Mon Sep 17 00:00:00 2001 From: kilo Date: Fri, 5 Jun 2026 21:16:48 -0300 Subject: [PATCH 10/25] fix(i18n): restore toast copy, preserve base path, remove all built-in switcher variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups from the second Codex review pass: 1. After the i18n.ts refactor, the messages object lost the hint/action/dismiss fields, so the first-visit toast rendered blank. Add them back to the i18n.ts messages map so the toast and the switcher share one source of truth for the labels. 2. targetFor() returns root-relative paths like '/es/' which break when the site is built with IS_FORK=1 (base is /libremesh.github.io). The toast and switcher now prepend siteData.value.base before assigning to window.location.href, so both fork-style and root-style builds work. 3. VitePress renders the language switcher in three places — the desktop nav (.VPNavBarTranslations), the mobile overflow menu (.group.translations inside .VPNavBarExtra), and the mobile screen (.VPNavScreenTranslations). The previous fix only removed the first. removeBuiltInSwitchers() now drops all three, and re-runs on every SPA route change so it survives client-side navigation. --- docs/.vitepress/theme/LanguageBanner.vue | 13 +++-- docs/.vitepress/theme/LanguageSwitcher.vue | 57 +++++++++++++++++----- docs/.vitepress/theme/i18n.ts | 11 +++-- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/docs/.vitepress/theme/LanguageBanner.vue b/docs/.vitepress/theme/LanguageBanner.vue index 919bde1c..768c166c 100644 --- a/docs/.vitepress/theme/LanguageBanner.vue +++ b/docs/.vitepress/theme/LanguageBanner.vue @@ -1,14 +1,21 @@