velxio/frontend/src/components/layout/AppHeader.tsx

378 lines
14 KiB
TypeScript
Raw Normal View History

import { useState, useRef, useEffect } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
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 { useTranslation } from 'react-i18next';
import { useAuthStore } from '../../store/useAuthStore';
import { useProjectStore } from '../../store/useProjectStore';
import { ShareModal } from './ShareModal';
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 { LanguageSwitcher } from './LanguageSwitcher';
import { useLocalizedHref, useCurrentLocale } from '../../i18n/useLocalizedNavigate';
import { blogUrlFor } from '../../i18n/path';
import { trackVisitGitHub, trackVisitDiscord } from '../../utils/analytics';
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
import type { AutoSaveState } from '../../hooks/useAutoSaveProject';
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 './LanguageSwitcher.css';
const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
const DISCORD_URL = 'https://discord.gg/3mARjJrh4E';
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
interface AppHeaderProps {
/** Optional auto-save state — when set, renders a save status indicator. */
autoSave?: AutoSaveState;
}
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
const SAVE_STATUS_COPY: Record<AutoSaveState['status'], { label: string; color: string }> = {
idle: { label: 'Saved', color: '#7d8590' },
dirty: { label: 'Unsaved changes', color: '#f0883e' },
saving: { label: 'Saving…', color: '#3fb950' },
saved: { label: 'Saved', color: '#3fb950' },
error: { label: 'Save failed', color: '#f85149' },
};
const AutoSaveIndicator: React.FC<{ state: AutoSaveState }> = ({ state }) => {
const meta = SAVE_STATUS_COPY[state.status];
const tip =
state.status === 'error' && state.errorMessage
? `Auto-save failed: ${state.errorMessage}`
: state.lastSavedAt
? `Last saved ${new Date(state.lastSavedAt).toLocaleTimeString()}`
: 'Auto-save ready';
return (
<div
title={tip}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '4px 10px',
fontSize: 12,
color: meta.color,
userSelect: 'none',
}}
>
<span
style={{
width: 7,
height: 7,
borderRadius: '50%',
background: meta.color,
opacity: state.status === 'saving' ? 0.7 : 1,
animation: state.status === 'saving' ? 'velxio-pulse 1s ease-in-out infinite' : 'none',
}}
/>
<span>{meta.label}</span>
</div>
);
};
export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave }) => {
const user = useAuthStore((s) => s.user);
const logout = useAuthStore((s) => s.logout);
const navigate = useNavigate();
const location = useLocation();
const currentProject = useProjectStore((s) => s.currentProject);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
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
const { t } = useTranslation();
const localize = useLocalizedHref();
const currentLocale = useCurrentLocale();
useEffect(() => {
const handler = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setDropdownOpen(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
// Close mobile menu on route change
useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);
const handleLogout = async () => {
setDropdownOpen(false);
await logout();
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
navigate(localize('/'));
};
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
const isActive = (path: string) =>
location.pathname === localize(path) ? ' header-nav-link-active' : '';
return (
<header className="app-header">
<div className="header-content">
<div className="header-left">
{/* Brand */}
<div className="header-brand">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#0071e3"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="5" y="5" width="14" height="14" rx="2" />
<rect x="9" y="9" width="6" height="6" />
<path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4" />
</svg>
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
<Link to={localize('/')} style={{ textDecoration: 'none', color: 'inherit' }}>
<span className="header-title">Velxio</span>
</Link>
</div>
{/* Main nav links (desktop) */}
<nav className={'header-nav-links' + (menuOpen ? ' header-nav-open' : '')}>
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
<Link to={localize('/')} className={'header-nav-link' + isActive('/')}>
{t('header.nav.home')}
</Link>
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
<Link to={localize('/docs')} className={'header-nav-link' + isActive('/docs')}>
{t('header.nav.documentation')}
</Link>
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
<Link to={localize('/examples')} className={'header-nav-link' + isActive('/examples')}>
{t('header.nav.examples')}
</Link>
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
<Link to={localize('/editor')} className={'header-nav-link' + isActive('/editor')}>
{t('header.nav.editor')}
</Link>
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
<Link to={localize('/about')} className={'header-nav-link' + isActive('/about')}>
{t('header.nav.about')}
</Link>
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
<a
href={blogUrlFor(currentLocale)}
className="header-nav-link"
rel="noopener"
>
{t('header.nav.blog')}
</a>
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="header-nav-link"
onClick={trackVisitGitHub}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
style={{ flexShrink: 0 }}
>
<path d="M12 2C6.477 2 2 6.484 2 12.021c0 4.428 2.865 8.185 6.839 9.504.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.605-3.369-1.342-3.369-1.342-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.026 2.747-1.026.546 1.378.202 2.397.1 2.65.64.7 1.028 1.595 1.028 2.688 0 3.848-2.338 4.695-4.566 4.944.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.203 22 16.447 22 12.021 22 6.484 17.523 2 12 2z" />
</svg>
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
{t('header.nav.github')}
</a>
<a
href={DISCORD_URL}
target="_blank"
rel="noopener noreferrer"
className="header-nav-link header-nav-discord"
onClick={trackVisitDiscord}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
style={{ flexShrink: 0 }}
>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.043.032.053a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
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
{t('header.nav.discord')}
</a>
</nav>
</div>
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
{/* Right: language + share + auth + mobile hamburger */}
<div className="header-right">
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
<LanguageSwitcher />
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
{/* Auto-save status only when a project is loaded and the editor
page mounted the hook */}
{autoSave && currentProject && <AutoSaveIndicator state={autoSave} />}
{/* Share button — visible when a project is loaded */}
{currentProject && location.pathname === '/editor' && (
<button
onClick={() => setShowShareModal(true)}
style={{
background: 'transparent',
border: '1px solid #555',
borderRadius: 4,
padding: '4px 10px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
color: '#ccc',
fontSize: 13,
}}
title="Share project"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="18" cy="5" r="3" />
<circle cx="6" cy="12" r="3" />
<circle cx="18" cy="19" r="3" />
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
</svg>
Share
</button>
)}
{/* Auth UI */}
{user ? (
<div style={{ position: 'relative' }} ref={dropdownRef}>
<button
onClick={() => setDropdownOpen((v) => !v)}
style={{
background: 'transparent',
border: '1px solid #555',
borderRadius: 20,
padding: '3px 10px 3px 6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 6,
color: '#ccc',
fontSize: 13,
}}
>
{user.avatar_url ? (
<img
src={user.avatar_url}
alt=""
style={{ width: 22, height: 22, borderRadius: '50%' }}
/>
) : (
<div
style={{
width: 22,
height: 22,
borderRadius: '50%',
background: '#0e639c',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
color: '#fff',
fontWeight: 600,
}}
>
{user.username[0].toUpperCase()}
</div>
)}
<span className="header-username-text">{user.username}</span>
</button>
{dropdownOpen && (
<div
data-velxio-slot="user-menu"
style={{
position: 'absolute',
right: 0,
top: '110%',
background: '#252526',
border: '1px solid #3c3c3c',
borderRadius: 6,
minWidth: 150,
zIndex: 100,
boxShadow: '0 4px 12px rgba(0,0,0,.4)',
}}
>
<Link
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
to={localize(`/${user.username}`)}
onClick={() => setDropdownOpen(false)}
style={{
display: 'block',
padding: '9px 14px',
color: '#ccc',
textDecoration: 'none',
fontSize: 13,
}}
>
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
{t('header.auth.myProjects')}
</Link>
<div style={{ borderTop: '1px solid #3c3c3c' }} />
<button
onClick={handleLogout}
style={{
width: '100%',
background: 'none',
border: 'none',
padding: '9px 14px',
color: '#ccc',
textAlign: 'left',
cursor: 'pointer',
fontSize: 13,
}}
>
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
{t('header.auth.signOut')}
</button>
</div>
)}
</div>
) : (
<div style={{ display: 'flex', gap: 8 }}>
<Link
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
to={localize('/login')}
style={{
color: '#ccc',
padding: '4px 10px',
fontSize: 13,
textDecoration: 'none',
border: '1px solid #555',
borderRadius: 4,
}}
>
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
{t('header.auth.signIn')}
</Link>
<Link
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
to={localize('/register')}
style={{
color: '#fff',
padding: '4px 10px',
fontSize: 13,
textDecoration: 'none',
background: '#0e639c',
borderRadius: 4,
}}
>
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
{t('header.auth.signUp')}
</Link>
</div>
)}
{/* Mobile hamburger */}
<button
className="header-hamburger"
onClick={() => setMenuOpen((v) => !v)}
aria-label="Toggle menu"
>
<span />
<span />
<span />
</button>
</div>
</div>
{showShareModal && <ShareModal onClose={() => setShowShareModal(false)} />}
</header>
);
};