The 12 repos under third-party/ were tracked as gitlinks (mode 160000)
without a matching .gitmodules file, causing 'git submodule' errors and
broken links in the GitHub UI (issue #164).
These repos are reference-only clones and are NOT required to run Velxio
(npm packages @wokwi/elements, avr8js, rp2040js cover all runtime needs).
Removing them from git tracking and ignoring the directory entirely so:
- New cloners get a clean repo with no broken submodule warnings
- Existing local clones keep their third-party/ folders untouched
- Optional manual clones remain possible for offline hacking
Closes#164
Replace Unix-only shell one-liner (mkdir -p / printf / cp -r) with a
Node.js ESM script (scripts/copy-monaco.mjs) that works on Windows,
macOS and Linux alike. The script still writes public/monaco/.gitignore
to keep copied assets out of git.
- postinstall now writes a '*' .gitignore into public/monaco/ so the
copied monaco-editor assets are never tracked as untracked files
- Also add public/monaco/ to frontend/.gitignore as a belt-and-suspenders
fallback for the same reason
- Add color picker button to SelectionActionBar for wire selections
- Toggle palette using WIRE_KEY_COLORS swatches
- Pass currentColor and onColorChange from SimulatorCanvas
- Reset showPalette on kind/onColorChange change (Copilot suggestion)
- Use t('editor.selectionBar.changeColor') for title/aria-label (Copilot suggestion)
- Add changeColor i18n key to all 9 locale files
Co-authored-by: naweiss <naweiss@users.noreply.github.com>
loadMicroPythonProgram only forwarded main.py (or files[0]) to the
bridge for raw-paste injection. Any auxiliary module the project
imported (mylib.py, drivers, etc.) never reached the device, so
`import mylib` died with ModuleNotFoundError.
Build a Python prelude that writes every other .py file to the
MicroPython filesystem via raw REPL, then runs main.py in the same
paste. JSON.stringify produces an ASCII-safe Python-compatible string
literal for the file body, which keeps the prelude inside the existing
chunked-UART path Esp32Bridge already uses to feed the 128-byte FIFO.
The RP2040 path was already multi-file via sim.loadMicroPython(files),
so it stays untouched.
Reproduces with the project shared in the bug report:
https://velxio.dev/project/ac7e285c-8dc3-4d51-8751-b4aba9912f9e
Block 9 added `const { t } = useTranslation()` at line 50 but forgot the
matching `import { useTranslation } from 'react-i18next'`. The component
then crashes the moment a user clicks a sensor on the canvas with
`Uncaught ReferenceError: useTranslation is not defined`, taking the
whole simulator render tree down.
components-metadata.json is shaped { version, components: [...] }, not a
flat array. The previous test assumed the latter and crashed on
default.find at module load on master, breaking CI for every PR.
The common bundle ballooned to 30KB after Block 15 added the AboutPage
prose, putting Russian translations past DeepSeek's 8192-token output
cap. The editor + about sub-trees (the two heaviest, ~15KB combined)
move to a new common2.json file. Both files now sit at 12-18KB and
translate cleanly.
i18n bootstrap merges common2 into the same common namespace at
load time and lazy-loads it per locale, so every existing t('editor.*')
and t('about.*') call keeps resolving without source changes.
All 9 locales regenerated via DeepSeek. Closes the gap left by the
Blocks 15+16 commit where only zh-cn/common had been refreshed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AboutPage: ~28 prose blocks across Story / How It Works / Open Source /
Creator / Releases / Quote / Community sections; <Trans> for paragraphs
with inline <strong>, <em>, <a> markup.
15 SEO landing pages now use t() for all user-facing copy under
seo.<page>.* keys: CircuitSimulatorPage, SpiceSimulatorPage,
ElectronicsSimulatorPage, CustomChipSimulatorPage, Attiny85SimulatorPage,
ArduinoSimulatorPage, ArduinoEmulatorPage, AtmegaSimulatorPage,
ArduinoMegaSimulatorPage, Esp32SimulatorPage, Esp32S3SimulatorPage,
Esp32C3SimulatorPage, RaspberryPiPicoSimulatorPage,
RaspberryPiSimulatorPage. Code blocks, FQBNs, JSON-LD schema strings
intentionally stay in English.
The seo bundle (67KB English source) is split into 4 balanced files
(seo.json + seo2.json + seo3.json + seo4.json, ~17KB each) so each
DeepSeek translation request stays inside the 8192-token output cap.
i18n bootstrap merges all 4 halves under the seo.* keyspace.
Translations: 8 locales × 4 seo bundles all regenerated via DeepSeek.
common.json (now 30KB after about additions) only has zh-cn refreshed
so far — the remaining 7 locales' common.json need a follow-up pass
(the bundle is at the edge of DeepSeek's output limit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A user reported on Discord: "the Velxio Console doesn't update anything,
it just waits until the very end and displays everything in one go".
True for the async compile path — /compile/status only carried `state`
and the final `result`, so the editor's CompilationConsole stayed empty
during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once
when the build finished.
This wires live build output through the whole stack.
Backend (espidf_compiler.py)
- New _run_with_streaming() helper. When a progress_callback is provided
it spawns the subprocess via Popen + stdout/stderr drain threads and
invokes the callback line-by-line. When None it falls back to the
existing subprocess.run(capture_output=True) one-shot path so the
unit-test code that doesn't care about live output is unaffected.
- compile() and _compile_in_dir() take an optional ProgressCallback.
- _run_cmake / _run_ninja closures now go through _run_with_streaming
with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both
stream now; the ninja output is the one users actually want to watch.
Backend (compile.py)
- _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines
on_progress_line(line) which appends to it. Buffer capped at 256 KB
(tail kept) so a runaway build can't OOM the FastAPI process.
- The buffer is preserved on both the success and the error path so
late polls still see the log even after state transitions to
done/error.
- /compile/status now returns the buffer as a `stdout` field.
CompileStatusResponse gains the field with default '' so old clients
that don't read it still work.
Frontend (compilation.ts)
- compileCode() takes a 4th argument: optional CompileProgress
callback fired every poll while state ∈ {pending, running}. Carries
the cumulative stdout (caller computes deltas) plus elapsed seconds.
- Surfaces the new `stdout` field of /compile/status and forwards it
to the callback. Errors thrown from the callback are swallowed —
a faulty UI hook must never break the polling loop.
Frontend (EditorToolbar.tsx)
- Both compileCode() call sites (Run and Compile-All) now pass an
onProgress callback. It tracks `lastStreamedLen` per-compile, splits
each new delta on newlines, and appends them as `info`-typed
CompilationLog entries via setCompileLogs. The Compile-All flow
prefixes each line with the board label so multi-board builds stay
readable.
- After the build settles, the existing parseCompileResult call still
runs and appends the structured analysis on top of the live stream
— that's where FAILED-block detection + the `error`-typed entries
that drive the auto-switch-to-errors filter live.
Net effect on the user complaint: cold ESP-IDF builds now show the
ninja [N/1483] progress lines streaming into the console as they
happen, instead of staring at an empty panel for 5-7 minutes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hit the 0.5 GB Actions storage quota today. Two-pronged fix.
1. docker-publish.yml: cache-to switched from mode=max to mode=min.
With mode=max, buildx pushes every intermediate layer of the
multi-stage build (qemu-provider, espidf-builder, frontend-builder,
final stage) into the GHA cache. For our image that's easily
500 MB-1 GB per cache update. mode=min stores only the layers used
by the final image; incremental rebuilds still hit the cache for
the meaningful steps but the footprint drops by roughly 60-70%.
2. actions-cache-cleanup.yml (new workflow):
- Weekly schedule (Sun 04:00 UTC): deletes every cache older than
14 days. Catches stale entries from deleted branches.
- On `pull_request: closed`: deletes caches scoped to that PR's
branch ref AND the merge ref. Buildx + actions/cache scope per
branch, so a closed PR's caches are immediately stale — without
this they linger until the GHA-default 7-day eviction.
- Manual `workflow_dispatch` for one-shot runs when storage is
already over.
Permissions: each job sets `actions: write` (the minimum needed for
cache deletion). No GH_TOKEN secret required; the default
GITHUB_TOKEN already has the scope.
Quota math after this lands:
Before: every push to master = +500 MB-1 GB cache, kept 7 days
→ quota fills in 1-2 builds.
After: every push to master = +200-400 MB cache, plus old branches
actively swept; 0.5 GB stays comfortable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A user reported on Discord that an ESP32 Blink compile on a fresh
`docker run -d ghcr.io/.../velxio:master` (no -v flags) takes 8-9
minutes — every single time. Same hardware that the local project
checkout flies through in 5-30 seconds with the new persistent build
dir + ccache pipeline.
Root cause: `docker run` without `-v` mounts gets nothing persistent.
ccache + persistent build dir live in /var/cache/ccache and
/var/lib/velxio-build, both wiped on every `docker rm` (which the
user explicitly did when troubleshooting). docker-compose users get
the volumes via the compose file; standalone users got nothing
because the Dockerfile didn't declare them.
This PR closes that gap.
Dockerfile.standalone
- VOLUME ["/app/data", "/root/.arduino15", "/root/Arduino",
"/var/cache/ccache", "/var/lib/velxio-build"]
Anonymous volumes are now created automatically when the user runs
the image without -v. They survive `docker stop`/`docker start`/
`docker rm` (only `docker rm -v` or `docker volume prune` removes
them). Users can still pass `-v` for named volumes — explicit
mounts always win over the VOLUME directive.
- Replace `ccache --set-config max_size 8G` (RUN, written to
/var/cache/ccache/ccache.conf which the volume mount masks at
runtime) with `ENV CCACHE_MAXSIZE=8G` etc. — env vars override any
conf-file value on every ccache invocation, so the 8 GB cap
actually applies at runtime regardless of what's in the volume.
README.md
- Update both the quick-start docker run and the detailed self-host
section to include all five volumes.
- Add a note explaining what each volume is for and that without them,
cold compile times are 5-7 min vs the 5-30 s warm path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire useTranslation() + Trans into DocsPage.tsx. ~330 user-facing
strings across 13 sections (intro, getting-started, emulator, riscv,
esp32, rp2040, rpi3, components, roadmap, architecture, third-party,
mcp, setup) plus sidebar nav + page chrome are now keyed under docs.*.
Strings with inline <a>, <code>, <strong>, <em> use the <Trans/>
component with mapped slots; bare prose uses t().
Code blocks, FQBNs, hex addresses, library names visible as link text,
and JSON-LD schema strings stay in English on purpose.
Internal Link to=... wrapped with localize() so /es/docs/... etc.
keep their locale prefix.
The English docs bundle is split in half (docs.json ~22KB +
docs2.json ~22KB) so each fits inside DeepSeek's 8192-token output
window. The i18n bootstrap merges both halves into the docs.* keyspace
under the default common namespace.
All 9 locales regenerated via DeepSeek (parallel run for the two
namespaces).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire useTranslation() into Velxio2Page and Velxio25Page: hero badge,
accent, subtitle, CTAs, board/example/outcome cards, OSS section, and
footer links all keyed under landing.v2.* and landing.v25.*.
Split en/common.json (34KB) into common.json (25KB) + releases.json
(9KB) so each translation request stays inside DeepSeek's 8192-token
output cap. i18n bootstrap merges both bundles into the default common
namespace at load time, lazy loader fetches both per locale.
translate-i18n.mjs: set max_tokens=8192 + response_format json_object
on the DeepSeek call so future bundles closer to the cap don't get
silently truncated.
All 9 locales regenerated via DeepSeek (fr/de/es/it/pt-br/zh-cn/ja/ru).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().
AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
table columns, Activity / Edit / Delete actions, empty state,
delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
table columns, public/private badge labels, delete action +
confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
fall back through t().
UserProfilePage
- "New project" CTA, loading + empty + not-found states,
"Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
project" button stays in Spanish.
PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
tier note + GitHub source note). Inline links wrapped via
the Trans component so the link surface stays clickable in
every locale without each translation having to re-write the
HTML.
EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
toggle (Show / Hide), View mode aria-label, view-mode
segmented control labels (Code / Both / Circuit), and the
three "Drag to resize" handle tooltips on the panel splitters.
Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
`npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
whole bundle, sameShape() validates each output before write).
This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
This commit closes the cluster of small editor surfaces that touch
the active simulation experience. Every visible control on these
panels now reads from t() keys.
Translated:
- Oscilloscope panel (title, Add Channel button + tooltip, Time/div
label, Run / Pause toggle copy + tooltips, Clear, empty-state copy
+ hint, per-channel remove tooltip).
- ComponentPropertyDialog (close, pin-roles header with two
wire-mode variants, Arduino Pin label, rotate / delete buttons +
the inline confirm-delete prompt with name interpolation).
- SelectionActionBar (toolbar aria-label, Rotate / Delete / Deselect
with kind-aware delete labels for wire / component / board).
- CompilationConsole (Output title, error / warning badge counts
with i18next pluralisation, filter dropdown, autoscroll label,
Clear + Close icon tooltips, empty-state).
- CustomChipDialog (header with chipName interpolation, Examples /
Editor tabs, Attributes panel header, compile status messages
including the "✓ Compiled — N KB" success line, footer
Cancel / Save & Place / Compile first buttons).
- SensorControlPanel (close button).
- BoardPickerModal (Add Board heading).
Translation pipeline
- en.json gets the new keys hand-curated.
- The 8 non-English locales were auto-translated via DeepSeek
using the existing scripts/translate-i18n.mjs pipeline (one
--force run, ~1 min total). Output validated with sameShape()
before write so any LLM-introduced key drift would have failed
loudly.
Quality note
- DeepSeek's translations now cover the entire bundle, including
earlier hand-translated content. Tone may differ slightly from
the prior hand passes but the meaning is consistent and brand /
technical terms (Velxio, ngspice-WASM, ATmega328P, ESP32-C3,
etc.) are preserved unchanged in every locale per the prompt
invariants.
The previous bump to 8G (PR #153) didn't take effect on prod. Verified
post-deploy:
Cache size (GB): 2.0 / 2.0 (99.95%)
Cause: $CCACHE_DIR (/var/cache/ccache) is a named docker volume. Any
config we wrote into it via `ccache --set-config max_size 8G` during
the RUN step gets masked at runtime by the mount of the existing
volume — which still contained the original 2 GB config from when the
cache was first populated.
Fix: set CCACHE_MAXSIZE / CCACHE_COMPRESS / CCACHE_COMPRESSLEVEL as
ENV in the Dockerfile. ccache reads those at runtime and they
override any conf-file value, including whatever stale config is
sitting in the volume.
After this lands and the submodule bumps, `ccache -s` should report
the new 8 GB max immediately on container start without needing to
wipe or re-init the cache volume.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical confirmation from a smoke test on prod after PR #152
(persistent build dir + dedup) landed:
Cache size (GB): 2.0 / 2.0 (99.96%)
Hits: 1 / 86886 ( 0.00%)
Misses: 86885 / 86886 (100.0%)
The cap was set to 2G when ccache was first wired in (PR #149) under
the assumption that base ESP-IDF would fit. With arduino-esp32 +
external Arduino libraries (Adafruit BMP280 + BusIO + Unified_Sensor +
GFX + …) the cacheable object set comfortably exceeds that — we
observed 86k misses while the cache was permanently at 99.96% full,
meaning entries get evicted before subsequent compiles can hit them.
Disk usage on the prod VPS at /var/cache/ccache will rise from ~2 GB
to ~6-8 GB. The host has plenty of headroom and the named volume sits
on the same partition as the rest of /var. Compression at level 6 is
already on, so the disk impact is the on-disk size after compression.
The 13-second warm compile we measured today already proves the bulk
of the perf win came from ninja's incremental cache + persistent build
dir; this is the cherry on top that lets ccache start contributing
across container restarts (when ninja's in-build-dir cache is fresh
but ccache should still hit at the C/C++ object level).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InstallLibrariesModal — auto-install prompt that fires when an
example needs libraries:
- Title, subtitle (with the "Installing X of Y" progress
interpolation, the all-done success state, and the singular /
plural prompt explaining the requirement count).
- Per-row status badges (pending / installing… / installed /
error) plus the Wokwi-hosted-library tooltip.
- Footer buttons: Close / Skip / "Install All ({{count}})" with
loading variant.
SerialMonitor — multi-board tabbed serial console:
- Empty-state when no board is on the canvas.
- Right-side tab controls: Autoscroll checkbox, Clear button +
tooltip.
- The "(Open IoT Gateway)" inline link rendered next to detected
AP IP addresses.
- Output-area placeholders for the running-but-no-data and
before-start states.
- Send button + input placeholder (different copy for MicroPython
REPL vs raw Serial input).
- The line-ending dropdown options (None / Newline / Carriage
return / Both).
Hand-translated for all 9 locales. Hotkeys (Ctrl+C) and dropdown
values stay untranslated (constants the firmware reads).
Pending in Phase 3:
- Oscilloscope panel, custom-chip dialog, sensor control panel.
- ComponentPropertyDialog (per-component property forms).
- Admin / Profile / Project pages.
- Long-form docs prose (DocsPage 2715 lines, AboutPage long
paragraphs).
- 15 SEO landing pages (intentionally English for keyword targeting).
ComponentPickerModal — the "Add Component" dialog:
- Header (title + close button), search input placeholder + clear,
category tabs (All Components / Boards), loading + empty-state
copy, "Clear filters" button.
LibraryManagerModal — the Arduino library browser:
- Window title, Search / Installed tabs, filter input placeholder.
- Search-tab states: searching-for-query, generic loading, no
results (with optional query interpolation).
- Per-library row: "by {{author}}" caption, Install / Installing /
Uninstall / Uninstalling button labels.
- Installed-tab empty-state with the prompt to use the Search tab.
Brand and product names left untouched ("LIBRARY MANAGER" stays
all-caps in English; the localised variants follow each language's
convention for product UI titles). Hand-translated for all 9
locales.
InstallLibrariesModal still pending — it's the auto-install
prompt that fires when an example needs libraries; smaller scope
but lives in the same area.
The /examples gallery is fully localised:
- Header (heading + subtitle).
- Search input placeholder + aria-label + the clear button.
- Match-count tag with i18next pluralisation (handles _one /
_other and Russian's _few / _many).
- Category and Difficulty filter labels + their button labels
(basics / sensors / displays / communication / games / robotics
/ circuits; beginner / intermediate / advanced).
- Per-card "Copy shareable link" tooltip.
- Empty-state copy with two variants (with-search / without-
search) interpolating the search query.
- Reset-filters button.
- The library-install progress overlay copy from
ExamplesPage.tsx ("Installing libraries (N/M)") with done/total
interpolation.
Internal /editor link uses localize() so a Spanish reader who
clicks an example lands on /es/editor.
Hand-translated for all 8 non-English locales. Per-example titles
+ descriptions are NOT i18n yet — they live in the
src/data/examples* tables and would need a separate pipeline.
DocsPage (2715 lines of prose) deferred too — best handled by
running scripts/translate-i18n.mjs once the keys are extracted.
Three coordinated fixes that together close the "ESP-IDF compile takes
5-7 min every time" gap and prevent the failure mode where a user clicking
compile multiple times spawns six ninja processes that peel each other
apart on a modest VPS.
What was wrong
- /compile/start generated a fresh uuid4 every call, so 6 clicks = 6
independent builds racing each other. Saw load average 30 on the prod
VPS during a real BMP280 attempt today.
- No concurrency limit anywhere; asyncio.create_task() fired without
gating.
- ccache was wired in last week (PR #149) but reported 18,350 cacheable
calls and **0 hits** because the build dir was a fresh
tempfile.TemporaryDirectory(prefix='espidf_') per compile. The random
/tmp/espidf_<random>/ path baked into -I and -fmacro-prefix-map flags
→ different command line every compile → ccache hash miss every time.
What this PR does
1. Job deduplication (`backend/app/api/routes/compile.py`)
- New `_job_key(files, board_fqbn)` returns SHA-256 of normalised file
names + contents + board. Order-independent.
- New `JOB_BY_KEY: dict[str, str]` indexes hash → job_id.
- `compile_start` checks JOB_BY_KEY before spawning a new task; if a
job for this exact content is already pending or running, returns
the existing job_id (logs `[compile] dedup hit — reusing job <id>`).
- `_purge_expired_jobs` evicts both COMPILE_JOBS and JOB_BY_KEY,
keeping the index consistent. Edge case where two jobs share a key
(old finished, new running) is handled — only evict the key entry
if it still points at the purged job.
2. Concurrency control (`backend/app/api/routes/compile.py`)
- `_COMPILE_SEMAPHORE = asyncio.Semaphore(2)` global cap on
simultaneous compiles.
- `_target_lock(board_fqbn)` returns a per-target asyncio.Lock so
concurrent compiles to the SAME board (sharing the persistent build
dir) serialise. Different boards still run in parallel up to the
semaphore cap.
- `_compile_job` acquires sema → per-target lock → flips state to
`running` → calls `_run_compile`. Pending state now accurately
reflects "queued waiting for resources".
3. Persistent build dir (`backend/app/services/espidf_compiler.py`)
- New `_prepare_persistent_project_dir(idf_target)` materialises
`/var/lib/velxio-build/<target>/project/` from the template on
first use; on subsequent compiles it wipes only `main/` and
`user_libs/` (the per-compile parts) and leaves `build/` alone so
ninja's incremental cache + ccache .o files survive.
- Toolchain version sentinel (`.idf_version`) wipes the whole target
dir if the ESP-IDF or arduino-esp32 version changes — cached
objects from the old toolchain are no longer ABI-compatible.
- `compile()` is now a thin dispatcher: persistent path or fallback
to the legacy `tempfile.TemporaryDirectory()` flow. The actual
build logic was extracted into `_compile_in_dir()` so both paths
share one implementation, no duplication.
- Escape hatch: `VELXIO_PERSISTENT_BUILD_DIR=0` env var falls back
to the tempfile path without rebuilding the image. Critical for
production safety.
4. ccache normalisation (`Dockerfile.standalone`)
- + `ENV CCACHE_BASEDIR=/var/lib/velxio-build` makes ccache canonicalise
absolute paths under that prefix when computing the cache key.
Robustens hits against any future subdir rearrangement.
5. Docker compose (`docker-compose.yml`)
- + named volume `velxio-build:/var/lib/velxio-build` so the persistent
build dir survives `docker compose up -d --build`.
- + env `VELXIO_PERSISTENT_BUILD_DIR=1` (default ON; users disable
without rebuilding).
Expected impact
- Cold first compile per container per target: unchanged (~5-7 min).
- Same sketch re-compiled: ~2-5 s (everything cached).
- Different sketch, same target: ~5-30 s (only user code + new lib steps
rebuild; ESP-IDF base hits cache).
- Different sketch with new libraries: ~30-90 s (new lib component
compiles; rest hits cache).
- Concurrent clicks on same example: 1 build, others poll the same
job_id. No more six-ninja meltdown.
Tests
- `test/backend/unit/test_compile_dedup.py` covers `_job_key` stability +
variance and `_purge_expired_jobs` consistency (including the
"two jobs share a key" edge case).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The visible chrome of /about now reads from i18n in all 9 locales:
- Hero title + subtitle
- 7 section headings (Story / How It Works / Open Source Philosophy /
Creator / Recent releases / Community & Press / CTA)
- Final CTA card (title, subtitle, "Open Editor" button)
- Footer copy switched to t('footer.about') so it shows the AGPLv3
About-Velxio paragraph instead of the stale MIT/avr8js credit
- Footer + CTA Links wrapped in localize() so /es/about's "Open Editor"
routes to /es/editor
Long-form prose (Story body paragraphs, Open Source Philosophy
paragraphs, Creator bio, Releases blurbs, Personal-story quote, Press
section) deliberately stays in English in this commit. Each is a
multi-paragraph chunk that benefits from a curated translation pass
rather than an inline machine pass — slate it for a follow-up.
Tech-stack tags (Java, Python, React, Docker, etc.) and the creator's
name + role + GitHub/LinkedIn/Medium link captions stay untranslated:
all proper nouns / brand identifiers.
Both auth forms now go through t('auth.login.*') and
t('auth.register.*'):
- Title + subtitle, email / password / username labels with
username placeholder and password-min-length placeholder.
- Submit button toggles between idle and loading states.
- "or" divider, "Continue with Google" button, and the
switch-to-other-form footer link.
- Inline validation errors: reserved username, username regex,
password length, plus the generic catch-all from the API.
Internal /editor and /login / /register links wrapped in
localize() so a Spanish user who registers stays at /es/editor
after success.
AboutPage (519 lines) deferred to its own commit — too dense for
the same change.
The canvas header (the bar above the simulation area) and the
"Remove board?" confirmation dialog now read from i18n.
Translated:
- Status dot tooltip (Running / Stopped).
- Active board selector tooltip + "No board" placeholder + the
hint that prompts the user to add a board.
- Undo / Redo buttons: aria-label, dynamic title with the action
description and the empty-state fallback. Action descriptions
themselves stay untranslated (they come from the editor history
store as English literals — translating them would mean reaching
into a different store; deferred).
- Serial Monitor and Oscilloscope toggles (button title + label).
- Zoom in / out / reset-view buttons.
- Component count tooltip + Add Component button.
- The error-banner Dismiss button.
- "Remove board" item in the right-click menu, with a localised
"(N wires)" parenthetical via i18next pluralisation.
- The full removal confirmation dialog: title with board label
interpolation, body copy with optional connected-wires sentence,
Cancel + Remove buttons.
Pluralisation uses i18next's _one / _other (and _few / _many for
Russian) suffixes so wire counts read naturally per language.
Hand-translated for all 8 non-English locales. Untouched (deferred):
the property dialog, custom-chip dialog, sensor control panel, and
the various inline tooltips on board pins and wire endpoints —
those are denser and benefit from a separate pass.
The three modal dialogs that fire during the editor's routine save +
share + auth-required flows are now fully localised across all 9
locales.
LoginPromptModal
- Title, body, and the three buttons (Sign in / Create account /
Cancel). Sign in / Sign up Links use localize() so a Spanish
reader prompted to log in lands at /es/login rather than dropping
back to English.
SaveProjectModal
- Title (toggles between Save / Update), name + description fields
with placeholders, save button (toggles between Save / Update /
Saving…), Cancel button.
- Visibility toggle: Public / Private label + hint copy under the
icon.
- All four error paths now go through t() with a {{status}}
interpolation for the generic HTTP failure message.
ShareModal
- Title, public/private label + hint pair, "Make private" /
"Make public" toggle, Copy button, the warning shown when the
project is private, and the Close button.
Hand-curated translations for all 8 non-English locales. Status
codes (403) and shortcut markers preserved.
FileExplorer (sidebar)
- Workspace header label and the new-workspace / save-project icon
buttons now read from t('editor.fileExplorer.*').
- Per-board section: collapse / expand toggle, status dot tooltip
(Running / Compiled / Idle), per-board "new file" button, and the
composite "<board name> — click to edit" hover title (the board
name itself stays untranslated — it's a product noun like
"Arduino Uno").
- File rows: hover title with optional "(unsaved)" suffix,
unsaved-dot tooltip, and the right-click context menu's Rename /
Delete commands.
- Empty-state placeholder when no boards are on the canvas.
- The window.confirm() shown before deleting a file now reads from
t() too, so non-English users see the prompt in their language.
FileTabs (open-tabs strip above the editor)
- Per-tab close button title and the unsaved-changes dot tooltip.
- Inline confirm dialog when closing a modified file: prompt copy,
"Close anyway" and "Cancel" buttons.
Hand-translated for all 9 locales. Hotkey hints (Ctrl+S, Strg+S)
localised per German convention; other locales keep "Ctrl+S" as the
universally-recognised label.
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.
Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.
Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.