feat(i18n): react-i18next foundation + 9-locale support for header / footer
This is Phase 1 of multi-language support: the visible chrome (header,
footer, language switcher) and routing are wired up for all 9 locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru) — same set the blog at
velxio.dev/blog/ already supports. The Editor and the long-form
landing-page copy are still English-only and will be translated in a
follow-up.
Infrastructure
- frontend/src/i18n/config.ts: locale registry + LOCALE_META (htmlLang,
native name, og locale, dir). Mirrors pro/blog/src/i18n/config.ts so
cookie sync stays consistent.
- frontend/src/i18n/cookie.ts: read/write the velxio_locale cookie at
Path=/; Max-Age=1y; SameSite=Lax (Secure on HTTPS). The blog reads
the same cookie via an inline script in its Layout.astro.
- frontend/src/i18n/path.ts: getLocaleFromPath / stripLocaleFromPath /
localizedPath / switchLocale / blogUrlFor — match the blog's helpers
one-to-one.
- frontend/src/i18n/index.ts: i18next bootstrap. English bundle is
inlined synchronously for first paint; non-default locales are
lazy-loaded via dynamic import on demand. Initial locale is decided
in priority order URL > cookie > navigator > en.
- frontend/src/i18n/LocaleSync.tsx: top-level wrapper inside <Router>.
On every URL change loads the matching locale bundle, calls
i18n.changeLanguage, writes the cookie, and mirrors the locale onto
<html lang> and dir.
- frontend/src/i18n/useLocalizedNavigate.ts: useCurrentLocale,
useLocalizedHref, useLocalizedNavigate hooks for components that
build internal links.
Routing
- App.tsx: route table extracted to a single ROUTES array, then
registered twice — once at the root (default English) and once
nested under each non-default locale (`/<locale>/...`). Explicit
per-locale parent routes (rather than a generic `:lang` param) so
React Router never accidentally swallows a real top-level path
like `/circuit-simulator` as a locale segment.
Header / Footer
- LanguageSwitcher.tsx + .css: dropdown matching the blog's
LanguageSwitcher.astro. Globe icon + locale code on the trigger,
native names + ISO codes in the menu. Click → `switchLocale()`
rewrites the URL under the new locale; LocaleSync handles the
rest (load bundle, change language, write cookie).
- AppHeader.tsx: every nav label and the auth dropdown copy now
goes through `t('header.nav.*')`, `t('header.auth.*')`. All
internal Links wrapped with localize() so navigation stays
inside the active locale. Added a "Blog" link computed via
`blogUrlFor(currentLocale)` so /es/ → /blog/es/, etc.
- LandingPage.tsx: footer About-Velxio paragraph reads from
t('footer.about').
Translations (Phase 1 strings)
- frontend/src/i18n/locales/<locale>/common.json: nav labels, auth
buttons, footer About copy. Hand-translated for all 9 locales,
AGPLv3 / brand names preserved as-is.
Tooling
- frontend/scripts/translate-i18n.mjs: standalone Node script that
takes the en.json bundles and auto-translates them to the 8 other
locales via DeepSeek (primary) + Gemini (fallback). One LLM call
per (locale, namespace) pair. Run after extracting new strings
with `npm run translate:i18n`.
Phase 2 (deferred)
- Editor (toolbar, file explorer, simulator canvas, component picker,
library manager, error toasts) — hundreds of strings.
- Examples / Docs / About / Profile pages.
- The translate-i18n.mjs script is ready to handle these once the
strings have been extracted into JSON keys.
2026-05-09 10:40:37 +07:00
|
|
|
import { useEffect, type ReactElement } from 'react';
|
2026-03-06 20:14:50 +07:00
|
|
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
2026-03-07 01:32:24 +07:00
|
|
|
import { LandingPage } from './pages/LandingPage';
|
2026-03-04 08:25:52 +07:00
|
|
|
import { EditorPage } from './pages/EditorPage';
|
|
|
|
|
import { ExamplesPage } from './pages/ExamplesPage';
|
2026-03-12 18:17:29 +07:00
|
|
|
import { DocsPage } from './pages/DocsPage';
|
2026-03-06 20:14:50 +07:00
|
|
|
import { LoginPage } from './pages/LoginPage';
|
|
|
|
|
import { RegisterPage } from './pages/RegisterPage';
|
|
|
|
|
import { UserProfilePage } from './pages/UserProfilePage';
|
|
|
|
|
import { ProjectPage } from './pages/ProjectPage';
|
2026-03-07 06:38:06 +07:00
|
|
|
import { ProjectByIdPage } from './pages/ProjectByIdPage';
|
2026-03-07 09:46:36 +07:00
|
|
|
import { AdminPage } from './pages/AdminPage';
|
2026-04-14 01:22:08 +07:00
|
|
|
import { ExampleDetailPage } from './pages/ExampleDetailPage';
|
2026-03-23 23:32:15 +07:00
|
|
|
import { ArduinoSimulatorPage } from './pages/ArduinoSimulatorPage';
|
|
|
|
|
import { ArduinoEmulatorPage } from './pages/ArduinoEmulatorPage';
|
|
|
|
|
import { AtmegaSimulatorPage } from './pages/AtmegaSimulatorPage';
|
|
|
|
|
import { ArduinoMegaSimulatorPage } from './pages/ArduinoMegaSimulatorPage';
|
2026-04-29 20:28:31 +07:00
|
|
|
import { Attiny85SimulatorPage } from './pages/Attiny85SimulatorPage';
|
|
|
|
|
import { CircuitSimulatorPage } from './pages/CircuitSimulatorPage';
|
|
|
|
|
import { SpiceSimulatorPage } from './pages/SpiceSimulatorPage';
|
|
|
|
|
import { ElectronicsSimulatorPage } from './pages/ElectronicsSimulatorPage';
|
|
|
|
|
import { CustomChipSimulatorPage } from './pages/CustomChipSimulatorPage';
|
2026-03-24 04:30:42 +07:00
|
|
|
import { Esp32SimulatorPage } from './pages/Esp32SimulatorPage';
|
|
|
|
|
import { Esp32S3SimulatorPage } from './pages/Esp32S3SimulatorPage';
|
|
|
|
|
import { Esp32C3SimulatorPage } from './pages/Esp32C3SimulatorPage';
|
|
|
|
|
import { RaspberryPiPicoSimulatorPage } from './pages/RaspberryPiPicoSimulatorPage';
|
|
|
|
|
import { RaspberryPiSimulatorPage } from './pages/RaspberryPiSimulatorPage';
|
2026-03-24 04:47:12 +07:00
|
|
|
import { Velxio2Page } from './pages/Velxio2Page';
|
2026-04-24 21:42:47 +07:00
|
|
|
import { Velxio25Page } from './pages/Velxio25Page';
|
2026-03-29 02:26:29 +07:00
|
|
|
import { AboutPage } from './pages/AboutPage';
|
2026-05-05 20:24:01 +07:00
|
|
|
import { PricingPlaceholder } from './pages/PricingPlaceholder';
|
2026-03-06 20:14:50 +07:00
|
|
|
import { useAuthStore } from './store/useAuthStore';
|
feat(i18n): react-i18next foundation + 9-locale support for header / footer
This is Phase 1 of multi-language support: the visible chrome (header,
footer, language switcher) and routing are wired up for all 9 locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru) — same set the blog at
velxio.dev/blog/ already supports. The Editor and the long-form
landing-page copy are still English-only and will be translated in a
follow-up.
Infrastructure
- frontend/src/i18n/config.ts: locale registry + LOCALE_META (htmlLang,
native name, og locale, dir). Mirrors pro/blog/src/i18n/config.ts so
cookie sync stays consistent.
- frontend/src/i18n/cookie.ts: read/write the velxio_locale cookie at
Path=/; Max-Age=1y; SameSite=Lax (Secure on HTTPS). The blog reads
the same cookie via an inline script in its Layout.astro.
- frontend/src/i18n/path.ts: getLocaleFromPath / stripLocaleFromPath /
localizedPath / switchLocale / blogUrlFor — match the blog's helpers
one-to-one.
- frontend/src/i18n/index.ts: i18next bootstrap. English bundle is
inlined synchronously for first paint; non-default locales are
lazy-loaded via dynamic import on demand. Initial locale is decided
in priority order URL > cookie > navigator > en.
- frontend/src/i18n/LocaleSync.tsx: top-level wrapper inside <Router>.
On every URL change loads the matching locale bundle, calls
i18n.changeLanguage, writes the cookie, and mirrors the locale onto
<html lang> and dir.
- frontend/src/i18n/useLocalizedNavigate.ts: useCurrentLocale,
useLocalizedHref, useLocalizedNavigate hooks for components that
build internal links.
Routing
- App.tsx: route table extracted to a single ROUTES array, then
registered twice — once at the root (default English) and once
nested under each non-default locale (`/<locale>/...`). Explicit
per-locale parent routes (rather than a generic `:lang` param) so
React Router never accidentally swallows a real top-level path
like `/circuit-simulator` as a locale segment.
Header / Footer
- LanguageSwitcher.tsx + .css: dropdown matching the blog's
LanguageSwitcher.astro. Globe icon + locale code on the trigger,
native names + ISO codes in the menu. Click → `switchLocale()`
rewrites the URL under the new locale; LocaleSync handles the
rest (load bundle, change language, write cookie).
- AppHeader.tsx: every nav label and the auth dropdown copy now
goes through `t('header.nav.*')`, `t('header.auth.*')`. All
internal Links wrapped with localize() so navigation stays
inside the active locale. Added a "Blog" link computed via
`blogUrlFor(currentLocale)` so /es/ → /blog/es/, etc.
- LandingPage.tsx: footer About-Velxio paragraph reads from
t('footer.about').
Translations (Phase 1 strings)
- frontend/src/i18n/locales/<locale>/common.json: nav labels, auth
buttons, footer About copy. Hand-translated for all 9 locales,
AGPLv3 / brand names preserved as-is.
Tooling
- frontend/scripts/translate-i18n.mjs: standalone Node script that
takes the en.json bundles and auto-translates them to the 8 other
locales via DeepSeek (primary) + Gemini (fallback). One LLM call
per (locale, namespace) pair. Run after extracting new strings
with `npm run translate:i18n`.
Phase 2 (deferred)
- Editor (toolbar, file explorer, simulator canvas, component picker,
library manager, error toasts) — hundreds of strings.
- Examples / Docs / About / Profile pages.
- The translate-i18n.mjs script is ready to handle these once the
strings have been extracted into JSON keys.
2026-05-09 10:40:37 +07:00
|
|
|
import { LocaleSync } from './i18n/LocaleSync';
|
|
|
|
|
import { NON_DEFAULT_LOCALES } from './i18n/config';
|
2026-03-03 10:20:49 +07:00
|
|
|
import './App.css';
|
|
|
|
|
|
feat(i18n): react-i18next foundation + 9-locale support for header / footer
This is Phase 1 of multi-language support: the visible chrome (header,
footer, language switcher) and routing are wired up for all 9 locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru) — same set the blog at
velxio.dev/blog/ already supports. The Editor and the long-form
landing-page copy are still English-only and will be translated in a
follow-up.
Infrastructure
- frontend/src/i18n/config.ts: locale registry + LOCALE_META (htmlLang,
native name, og locale, dir). Mirrors pro/blog/src/i18n/config.ts so
cookie sync stays consistent.
- frontend/src/i18n/cookie.ts: read/write the velxio_locale cookie at
Path=/; Max-Age=1y; SameSite=Lax (Secure on HTTPS). The blog reads
the same cookie via an inline script in its Layout.astro.
- frontend/src/i18n/path.ts: getLocaleFromPath / stripLocaleFromPath /
localizedPath / switchLocale / blogUrlFor — match the blog's helpers
one-to-one.
- frontend/src/i18n/index.ts: i18next bootstrap. English bundle is
inlined synchronously for first paint; non-default locales are
lazy-loaded via dynamic import on demand. Initial locale is decided
in priority order URL > cookie > navigator > en.
- frontend/src/i18n/LocaleSync.tsx: top-level wrapper inside <Router>.
On every URL change loads the matching locale bundle, calls
i18n.changeLanguage, writes the cookie, and mirrors the locale onto
<html lang> and dir.
- frontend/src/i18n/useLocalizedNavigate.ts: useCurrentLocale,
useLocalizedHref, useLocalizedNavigate hooks for components that
build internal links.
Routing
- App.tsx: route table extracted to a single ROUTES array, then
registered twice — once at the root (default English) and once
nested under each non-default locale (`/<locale>/...`). Explicit
per-locale parent routes (rather than a generic `:lang` param) so
React Router never accidentally swallows a real top-level path
like `/circuit-simulator` as a locale segment.
Header / Footer
- LanguageSwitcher.tsx + .css: dropdown matching the blog's
LanguageSwitcher.astro. Globe icon + locale code on the trigger,
native names + ISO codes in the menu. Click → `switchLocale()`
rewrites the URL under the new locale; LocaleSync handles the
rest (load bundle, change language, write cookie).
- AppHeader.tsx: every nav label and the auth dropdown copy now
goes through `t('header.nav.*')`, `t('header.auth.*')`. All
internal Links wrapped with localize() so navigation stays
inside the active locale. Added a "Blog" link computed via
`blogUrlFor(currentLocale)` so /es/ → /blog/es/, etc.
- LandingPage.tsx: footer About-Velxio paragraph reads from
t('footer.about').
Translations (Phase 1 strings)
- frontend/src/i18n/locales/<locale>/common.json: nav labels, auth
buttons, footer About copy. Hand-translated for all 9 locales,
AGPLv3 / brand names preserved as-is.
Tooling
- frontend/scripts/translate-i18n.mjs: standalone Node script that
takes the en.json bundles and auto-translates them to the 8 other
locales via DeepSeek (primary) + Gemini (fallback). One LLM call
per (locale, namespace) pair. Run after extracting new strings
with `npm run translate:i18n`.
Phase 2 (deferred)
- Editor (toolbar, file explorer, simulator canvas, component picker,
library manager, error toasts) — hundreds of strings.
- Examples / Docs / About / Profile pages.
- The translate-i18n.mjs script is ready to handle these once the
strings have been extracted into JSON keys.
2026-05-09 10:40:37 +07:00
|
|
|
/**
|
|
|
|
|
* Single source of truth for the route tree. Each entry is registered
|
|
|
|
|
* twice in <Routes> below: once at the root (default locale) and once
|
|
|
|
|
* nested under each non-default locale prefix (e.g. `/es/editor`).
|
|
|
|
|
*
|
|
|
|
|
* Index entries (path === '') belong to the locale-prefixed parent's
|
|
|
|
|
* `index` slot — they render at exactly `/<locale>/`.
|
|
|
|
|
*/
|
|
|
|
|
const ROUTES: { path: string; element: ReactElement; index?: boolean }[] = [
|
|
|
|
|
{ path: '/', element: <LandingPage />, index: true },
|
|
|
|
|
{ path: 'editor', element: <EditorPage /> },
|
|
|
|
|
{ path: 'examples', element: <ExamplesPage /> },
|
|
|
|
|
{ path: 'examples/:exampleId', element: <ExampleDetailPage /> },
|
|
|
|
|
{ path: 'docs', element: <DocsPage /> },
|
|
|
|
|
{ path: 'docs/:section', element: <DocsPage /> },
|
|
|
|
|
{ path: 'login', element: <LoginPage /> },
|
|
|
|
|
{ path: 'register', element: <RegisterPage /> },
|
|
|
|
|
{ path: 'admin', element: <AdminPage /> },
|
|
|
|
|
// SEO landing pages — keyword-targeted
|
|
|
|
|
{ path: 'circuit-simulator', element: <CircuitSimulatorPage /> },
|
|
|
|
|
{ path: 'spice-simulator', element: <SpiceSimulatorPage /> },
|
|
|
|
|
{ path: 'electronics-simulator', element: <ElectronicsSimulatorPage /> },
|
|
|
|
|
{ path: 'custom-chip-simulator', element: <CustomChipSimulatorPage /> },
|
|
|
|
|
{ path: 'attiny85-simulator', element: <Attiny85SimulatorPage /> },
|
|
|
|
|
{ path: 'arduino-simulator', element: <ArduinoSimulatorPage /> },
|
|
|
|
|
{ path: 'arduino-emulator', element: <ArduinoEmulatorPage /> },
|
|
|
|
|
{ path: 'atmega328p-simulator', element: <AtmegaSimulatorPage /> },
|
|
|
|
|
{ path: 'arduino-mega-simulator', element: <ArduinoMegaSimulatorPage /> },
|
|
|
|
|
{ path: 'esp32-simulator', element: <Esp32SimulatorPage /> },
|
|
|
|
|
{ path: 'esp32-s3-simulator', element: <Esp32S3SimulatorPage /> },
|
|
|
|
|
{ path: 'esp32-c3-simulator', element: <Esp32C3SimulatorPage /> },
|
|
|
|
|
{ path: 'raspberry-pi-pico-simulator', element: <RaspberryPiPicoSimulatorPage /> },
|
|
|
|
|
{ path: 'raspberry-pi-simulator', element: <RaspberryPiSimulatorPage /> },
|
|
|
|
|
{ path: 'v2', element: <Velxio2Page /> },
|
|
|
|
|
{ path: 'v2-5', element: <Velxio25Page /> },
|
|
|
|
|
{ path: 'about', element: <AboutPage /> },
|
|
|
|
|
// Pricing — placeholder by default; private overlays portal-inject the real page
|
|
|
|
|
{ path: 'pricing', element: <PricingPlaceholder /> },
|
|
|
|
|
// Canonical project URL by ID
|
|
|
|
|
{ path: 'project/:id', element: <ProjectByIdPage /> },
|
|
|
|
|
// Legacy slug route — redirects to /project/:id
|
|
|
|
|
{ path: ':username/:projectName', element: <ProjectPage /> },
|
|
|
|
|
{ path: ':username', element: <UserProfilePage /> },
|
|
|
|
|
];
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
function App() {
|
2026-03-06 20:14:50 +07:00
|
|
|
const checkSession = useAuthStore((s) => s.checkSession);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
checkSession();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-03-03 10:20:49 +07:00
|
|
|
return (
|
2026-03-04 08:25:52 +07:00
|
|
|
<Router>
|
feat(i18n): react-i18next foundation + 9-locale support for header / footer
This is Phase 1 of multi-language support: the visible chrome (header,
footer, language switcher) and routing are wired up for all 9 locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru) — same set the blog at
velxio.dev/blog/ already supports. The Editor and the long-form
landing-page copy are still English-only and will be translated in a
follow-up.
Infrastructure
- frontend/src/i18n/config.ts: locale registry + LOCALE_META (htmlLang,
native name, og locale, dir). Mirrors pro/blog/src/i18n/config.ts so
cookie sync stays consistent.
- frontend/src/i18n/cookie.ts: read/write the velxio_locale cookie at
Path=/; Max-Age=1y; SameSite=Lax (Secure on HTTPS). The blog reads
the same cookie via an inline script in its Layout.astro.
- frontend/src/i18n/path.ts: getLocaleFromPath / stripLocaleFromPath /
localizedPath / switchLocale / blogUrlFor — match the blog's helpers
one-to-one.
- frontend/src/i18n/index.ts: i18next bootstrap. English bundle is
inlined synchronously for first paint; non-default locales are
lazy-loaded via dynamic import on demand. Initial locale is decided
in priority order URL > cookie > navigator > en.
- frontend/src/i18n/LocaleSync.tsx: top-level wrapper inside <Router>.
On every URL change loads the matching locale bundle, calls
i18n.changeLanguage, writes the cookie, and mirrors the locale onto
<html lang> and dir.
- frontend/src/i18n/useLocalizedNavigate.ts: useCurrentLocale,
useLocalizedHref, useLocalizedNavigate hooks for components that
build internal links.
Routing
- App.tsx: route table extracted to a single ROUTES array, then
registered twice — once at the root (default English) and once
nested under each non-default locale (`/<locale>/...`). Explicit
per-locale parent routes (rather than a generic `:lang` param) so
React Router never accidentally swallows a real top-level path
like `/circuit-simulator` as a locale segment.
Header / Footer
- LanguageSwitcher.tsx + .css: dropdown matching the blog's
LanguageSwitcher.astro. Globe icon + locale code on the trigger,
native names + ISO codes in the menu. Click → `switchLocale()`
rewrites the URL under the new locale; LocaleSync handles the
rest (load bundle, change language, write cookie).
- AppHeader.tsx: every nav label and the auth dropdown copy now
goes through `t('header.nav.*')`, `t('header.auth.*')`. All
internal Links wrapped with localize() so navigation stays
inside the active locale. Added a "Blog" link computed via
`blogUrlFor(currentLocale)` so /es/ → /blog/es/, etc.
- LandingPage.tsx: footer About-Velxio paragraph reads from
t('footer.about').
Translations (Phase 1 strings)
- frontend/src/i18n/locales/<locale>/common.json: nav labels, auth
buttons, footer About copy. Hand-translated for all 9 locales,
AGPLv3 / brand names preserved as-is.
Tooling
- frontend/scripts/translate-i18n.mjs: standalone Node script that
takes the en.json bundles and auto-translates them to the 8 other
locales via DeepSeek (primary) + Gemini (fallback). One LLM call
per (locale, namespace) pair. Run after extracting new strings
with `npm run translate:i18n`.
Phase 2 (deferred)
- Editor (toolbar, file explorer, simulator canvas, component picker,
library manager, error toasts) — hundreds of strings.
- Examples / Docs / About / Profile pages.
- The translate-i18n.mjs script is ready to handle these once the
strings have been extracted into JSON keys.
2026-05-09 10:40:37 +07:00
|
|
|
<LocaleSync>
|
|
|
|
|
<Routes>
|
|
|
|
|
{/* Default locale (English) — no URL prefix. */}
|
|
|
|
|
{ROUTES.map((r) =>
|
|
|
|
|
r.index ? (
|
|
|
|
|
<Route key="root" path="/" element={r.element} />
|
|
|
|
|
) : (
|
|
|
|
|
<Route key={r.path} path={`/${r.path}`} element={r.element} />
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/*
|
|
|
|
|
Non-default locales — same routes nested under `/<locale>/`.
|
|
|
|
|
We register one branch per locale rather than a `:lang` param
|
|
|
|
|
so React Router doesn't accidentally swallow real top-level
|
|
|
|
|
paths like `/circuit-simulator` as a locale segment.
|
|
|
|
|
*/}
|
|
|
|
|
{NON_DEFAULT_LOCALES.map((locale) => (
|
|
|
|
|
<Route key={`locale-${locale}`} path={`/${locale}`}>
|
|
|
|
|
{ROUTES.map((r) =>
|
|
|
|
|
r.index ? (
|
|
|
|
|
<Route key={`${locale}-root`} index element={r.element} />
|
|
|
|
|
) : (
|
|
|
|
|
<Route
|
|
|
|
|
key={`${locale}-${r.path}`}
|
|
|
|
|
path={r.path}
|
|
|
|
|
element={r.element}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</Route>
|
|
|
|
|
))}
|
|
|
|
|
</Routes>
|
|
|
|
|
</LocaleSync>
|
2026-03-04 08:25:52 +07:00
|
|
|
</Router>
|
2026-03-03 10:20:49 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App;
|