Add Spanish and Brazilian Portuguese locales#175
Open
luandro wants to merge 19 commits into
Open
Conversation
added 19 commits
June 5, 2026 19:16
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.
…rror 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 <script setup> and store the reactive route ref for use everywhere.
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.
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.
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.
…locales
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.
…miss 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 <meta name=description> 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.
…ts translatedPaths Codex (gpt-5.1-codex-max) flagged a P2 in the previous review pass: VitePress's built-in VPNavBarTranslations auto-generates links to /<locale>/<current-path> 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.
…but not committed The previous commit imported these two new files in index.ts but used 'git commit -am', which only stages tracked files. The new files stayed in the working tree, so the commit on the branch references files that don't exist there and the build fails on a clean checkout. Add them now.
…n switcher variants 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.
…plicitly Codex review caught that navEs()/navPtBr() point 'Guía' to /guide/packages-selection (English) without a hint, so a user on /es/ clicks a translated label and lands on an English page with no warning — and the translated /es/guide/connecting exists. Point 'Guía'/'Guia' at the first translated guide page (/<lang>/guide/connecting). For items whose target isn't translated yet, append an '(en)' / '(em)' hint to the label and keep the target as the English path, matching the link-out convention used in body text.
Codex flagged that the JS-based cleanup in LanguageSwitcher.vue only runs after client hydration, so the static SSR HTML still contains dead /<locale>/<current-path> hrefs in the mobile overflow menu (.group.translations) for no-JS visitors and crawlers. Extend the hide rule to all three locations where VitePress renders the built-in switcher: - .VPNavBarTranslations (desktop nav) - .VPNavScreenTranslations (mobile screen) - .group.translations (mobile overflow) - attribute selectors as a defensive catch-all The hrefs still exist in the markup (crawlers will hit 404s), but they are hidden visually for every visitor regardless of JS state. The remaining dead links shrink as more pages are translated — see TRANSLATING.md for the path map.
…a translation Codex caught that the toast said 'this page is also available in Español' on any English page, even when clicking would actually take the user to the locale home (because the current page has no translated counterpart in translatedPaths). Add hasTranslation() to i18n.ts and gate the toast on it. Now the toast only appears when the user is on a page that genuinely has a translated version — otherwise we stay silent and let the nav switcher do its job (it already handles the home fallback correctly).
Codex caught a runtime bug: in this VitePress version useData()
returns { site, theme, page, ... } (named 'site', not
'siteData'). The components were destructuring { siteData } which
is undefined, so withBase() always fell back to '/' and any
redirect on a base-path build (e.g. /libremesh.github.io) would
404 to the wrong place.
Destructure { site } from useData() in LanguageBanner.vue and
LanguageSwitcher.vue. Functionally equivalent for the root-style
build, and actually correct for the IS_FORK=1 build.
Codex found two bugs that both fire only on IS_FORK=1 builds (where base is /libremesh.github.io) and on the root build they silently worked by luck: 1. route.path includes the base prefix (e.g. /libremesh.github.io/pt-BR/foo), so stripLocale and currentLocaleOf never matched the /es or /pt-BR prefix. The custom switcher then rendered translated pages as 'EN' and language clicks misrouted to the locale home or double-prefixed the base. 2. goesToHome compared targetFor(loc, route.path) (returning /es/...) against cleanPath(stripLocale(route.path)) (returning /...). They always differed, so the 'home' hint showed on every non-English dropdown item even for pages with a real translation. Add stripBase(path, base) to i18n.ts and compute rootPath in both components. All locale helpers now see a root-relative path. Also fix goesToHome to compare against the locale home directly instead of against the always-different clean path, so the hint only shows for actual fallbacks.
Codex caught two P1/P2 bugs: 1. VitePress emits '/es/what-is-libremesh.html' but translatedPaths was returning the extensionless '/es/what-is-libremesh', which 404s on static hosting. Add .html to every non-home target. The home target stays as '/es/' (VitePress serves the directory index). 2. hasTranslation didn't include '/' in translatedPaths, so the first-visit toast was suppressed when the user landed on the English home page '/' even though the localized homes '/es/' and '/pt-BR/' exist. Add the home mapping so the homepage gets the same language suggestion as other translated pages.
The previous custom switcher used a bordered text button with a text short-code (EN/ES/PT) and a square dropdown panel — it didn't look anything like VitePress's built-in VPNavBarTranslations. Rebuild it as a flyout that matches the built-in styling: transparent button, vpi-languages globe icon + vpi-chevron-down, 0 12px padding, var(--vp-nav-height) tall, hover/aria-expanded drives menu visibility, themed panel (border-radius 12px, var(--vp-c-bg-elv), var(--vp-shadow-3)), themed menu links with active highlight. On screens < 1280px the switcher hides itself entirely, matching the built-in's @media rule. Mobile and the responsive 'extra' menu still get the built-in hidden via CSS, so the only switcher users ever see is this one.
…le screen variant Three things: 1. Drop the duplicate locale name from the dropdown. The previous template rendered a 'title' row (current locale, e.g. 'English') AND listed every locale in the items below, so the active one appeared twice. Remove the title; items alone are the source of truth and the active item is still highlighted via the 'active' class. 2. Reposition the switcher to the right of the theme (appearance) switcher. VitePress's nav has no slot between appearance and social, so reorder the flex children via CSS order: appearance order 0, lang-switcher order 1, social order 2, extra order 3. Add a '|' divider via a 1px-tall ::before pseudo-element on the switcher, matching the visual rhythm of the built-in nav. 3. Add a LanguageSwitcherMobile.vue and mount it in nav-screen-content-after so the language menu is reachable from the mobile full-screen menu too. Mirrors the built-in VPNavScreenTranslations layout: collapsible header with globe icon and current locale label, expanding to a list of locale buttons. The two components share i18n.ts so the path map stays in sync. Also remove the @media (min-width: 1280px) hide on the desktop switcher — flex order is no-op on small screens where the desktop nav is hidden, so the rule was redundant. The corresponding hide below 1280px on the built-in VPNavBarTranslations is what determines whether the desktop switcher shows; CSS only hides the built-in everywhere, the custom one in nav-bar-content-after inherits the same visibility from its flex parent (display: none on .content-body? no, the content-body is always shown — but the nav-bar-content-after slot content sits inside content-body which is always visible, just hidden when isScreenOpen? no...). The desktop switcher is visible at all sizes inside the desktop nav row, and the mobile screen switcher covers phones. So the @media hide is no longer needed.
…uilt-in The .lang-switcher::before pseudo-element had margin: 0 12px 0 0 (12px right, 0 left), making the divider flush against the lang-switcher's left edge. This created uneven spacing between the theme toggle and the divider. Changed to margin: 0 8px to match VitePress's built-in dividers, which use margin-right: 8px; margin-left: 8px; for equal 8px spacing on both sides.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the v1 of #172.
What's in this PR
docs/.vitepress/config.mts:esandpt-BRadded to thelocalesconfig with localizednavand a trimmedsidebar(only translated pages for now, grows as more land).what-is-libremeshgetting-startedguide/connectingfeaturesdocs/.vitepress/theme/LanguageBanner.vue): readsnavigator.language, on first visit suggests the matching locale with a dismissible banner. Choice remembered inlocalStorage. No jarring redirect, no content flash. Mounted via thelayout-topslot intheme/index.ts.docs/TRANSLATING.md: contributor guide with the link-out convention, glossary of terms to keep in English (Batman-adv, Babel, LiMe, LuCI, OpenWrt, …), and the steps to add a new language.(en)/(em)hint rather than 404ing — seedocs/es/getting-started.mdfor the pattern.Why this approach
localesdoes the heavy lifting: language switcher in the nav,<html lang>set per locale, URL routing under/es/and/pt-BR/. Zero new build pipeline.editLinkconfig already lands contributors on the right file.prepare_data.jsand number in the hundreds). Not realistic to translate by hand.Out of scope for this PR (intentional)
packages/andprofiles/pages.<link rel="alternate" hreflang="...">block (easy follow-up if wanted).Verification
pnpm buildpasses locally; both/es/and/pt-BR/render the four translated pages and the language switcher is visible in the nav.inBrowserguard).Follow-ups
guide/packages-selectionandreference/configuration).Refs #172.