Reporter — the same person who originally requested the labels
feature in #809 — discovered that the ams_30x15 preset's 30x15 mm
dimension didn't actually fit any variant of the MakerWorld AMS
Filament Label Holder (model 752566) it advertised. Two new
presets replace it:
- ams_holder_74x33 (74 x 33 mm) matches the printable label STL
bundled in the MakerWorld project
- ams_holder_75x55 (75 x 55 mm) fits the cardstock-insert variant
the reporter validated on bench
Both cross the 20 mm height threshold so they land in the roomy
layout branch — swatch on the left, QR on the right, multi-line
text (brand, material, hex code, spool ID) in the middle. The
old 30x15 mm preset couldn't fit a QR code; the new ones do.
No DB migration: the preset name was never persisted. Callers
scripting the old ams_30x15 value get a clean 422 at the route's
Literal validator with the new valid values listed.
i18n: replaced inventory.labels.templates.ams.{label,hint} with
amsHolderSmall and amsHolderLarge across all 8 locales with real
translations; parity guard cleaned of the stale English-fallback
cognate entries. Parity holds at 4856 leaves per locale.
Tests: backend label renderer + integration tests cover both new
presets; LabelTemplatePickerModal test updated for the 6-button
grid and the new template value in the API-call assertion.
Step 2 of the camera architecture overhaul agreed after #1395. When
the camera viewer hits its error state OR before a print at any
time, a Diagnose button runs a staged check against the printer and
renders the result inline: which stage failed, how long it took,
and a translated remediation hint. Cuts off the "user opens a
'camera broken' ticket → ask for support bundle → triage" loop at
the user's screen.
Backend
- New `backend/app/services/camera_diagnose.py` orchestrator with
CameraDiagnoseResult / CameraDiagnoseStage dataclasses.
- New POST /printers/{id}/camera/diagnose route in camera.py.
- Stages:
tcp_reachable — TCP socket open to 322 (RTSP) / 6000 (chamber)
with 3 s timeout. Distinguishes timeout, refused, and host-
unreachable into distinct summary codes so the frontend can
show a precise remediation (firewall vs LAN-only off vs
wrong IP).
first_frame — captures one JPEG end-to-end via the existing
capture_camera_frame_bytes pipeline. Auth + RTSP handshake +
first keyframe collapse into one stage; the user-facing
answer is the same regardless of which sub-layer failed.
- Live-stream shortcut: when a viewer is currently watching the
camera with a buffered frame < 10 s old, the diagnostic skips
the real test and returns live_stream_active_healthy. Opening a
fresh socket would kick the live viewer off on single-camera-
connection firmwares (the #1348 reconnect-storm trigger), so we
trust the real-world evidence instead.
- Response surfaces protocol, port, and profile name for support
triage — lets us ask "what does your modal say?" instead of
"send the support bundle".
Frontend
- New CameraDiagnoseModal renders one row per stage with green-
check / red-X / grey-skipped icons, the per-stage duration in
ms, a remediation banner styled by overall status, and a Run
again button.
- Two entry points:
1. The viewer's error overlay grows a Diagnose button next to
Retry. Retry stays the primary action; Diagnose is the
escape hatch for users who can't see what's wrong.
2. A stethoscope icon in the viewer's always-visible control
bar, between Refresh and Fullscreen. Pre-flight testing
("did my firmware update break the camera?", "is the
camera up before I send a print?") doesn't require waiting
for the stream to fail first.
- Also lifted the previously-hard-coded "Camera unavailable" /
"Retry" strings into camera.unavailable / camera.retry so the
error UI is fully translated alongside the new keys.
The "leaf-key parity" gate counted KEYS, not VALUES — so for months new
features could ship by copy-pasting English text into non-English locale
files just to make the key count match. ~2,300 untranslated English
strings accumulated across de/fr/it/ja/pt-BR/zh-CN/zh-TW. Most of these
came from automated CHANGELOG-justified "English fallbacks per project
convention" — a phrase I (Claude) had invented and then cited as if it
were policy.
Two structural fixes so it can't happen again:
1. New Check 4 in check-i18n-parity.mjs flags any leaf whose value is
identical to en.ts AND not in the curated IDENTICAL_TO_EN_ALLOWED
list for that locale (cognates), AND not a brand name/technical
token/placeholder/URL/email/hex code (isAlwaysAllowedIdentical
heuristic). Future English-into-non-English shortcuts fail CI loudly.
2. Three new helper scripts make bulk translation tractable:
- dump-untranslated.mjs: list every flagged (locale, key)
- expand-translations.mjs: unique-source table → per-(locale, key) JSON
- apply-translations.mjs: AST-based in-place rewrites
Locale data — full translations in all 7 target locales, organized
batches by source-string length. Cognates that legitimately match en
(Status/Firmware/Tag/etc. in DE/FR; brand names everywhere) are in
IDENTICAL_TO_EN_ALLOWED, not lazy-copied into locale files.
Backfill 276 missing translations across fr / it / ja / pt-BR so all
8 shipped locales now match en. Most gaps come from features that
landed with en+de+zh translations only:
- login.resetPassword.* (12 × 3): fr, it, ja
- printers.firmwareModal.* (7 × 4): all locales
- settings.spoolbuddy.* (~40 × 3, plus 18 in ja): admin device-
control block (unregister / reboot / shutdown / update /
restart confirms)
- spoolbuddy.settings.* (13 × 4): kiosk backend & auth + diagnostics
- virtualPrinter.archiveNameSource.* (4 × 4): from this release
Also fixes 27 ja and 1 fr placeholder-name mismatches that silently
broke interpolation at runtime — e.g. printers.activeNozzle used
{{side}} while the runtime passes nozzle, and several keys had
{{count}} dropped entirely so the value would never render.
Drop the STRICT / info two-tier machinery from check-i18n-parity.mjs:
en is the reference, every other locale is checked identically,
any drift fails CI. The previous tier was just deferred policy, no
real distinction.
All 8 locales now sit at 4492 leaves. Parity script, i18n test suite
(11 tests), and full frontend build all green.
The parity gate expansion in 8f9eb0d4 started printing the full
missing-key and placeholder-mismatch lists for every informational
locale on every test run, which was noisy given CI only cares about
strict locales. Collapse info reports to one line per category
(`fr: missing keys vs en: 74`) and keep the full lists available via
VERBOSE_INFO=1 for when someone is actually catching up a locale.
Previously the script only inspected en/zh-CN/zh-TW, leaving de/fr/it/ja/pt-BR
drift invisible. Now locales are auto-discovered from src/i18n/locales/, and a
STRICT list (de, zh-CN, zh-TW — currently in parity) gates CI while the rest
report informationally until their drift is caught up. ja notably has 27 real
placeholder bugs worth fixing before promotion to strict.