Commit Graph
76 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 5.5 6c6fb32d8d i18n: rows for the beta_94 strings
Temperature in Fahrenheit, Up to 15 characters and Favorites only in all 14
languages; the full-Home toast as an English placeholder like the other Home
widget strings. The V4 keeps its pinned beta_92 table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj Schittecat 133df4ca7f Merge PR #635 (Confituurke): feat(ui): pick your advert position on the map 2026-10-13 09:18:20 +02:00
Kaj Schittecat 01182d1be1 Merge PR #624 (jcyrio): Pager: Fn+V pastes the clipboard (no touch path to the edit menu) 2026-10-13 09:18:18 +02:00
Confituurke 15ca3f8902 feat(ui): pick your advert position on the map
Typing a latitude and longitude by hand is the only way to set a
position on a node without GPS, and few people know their coordinates to
six decimals. The map already knows where its centre is, so let it do
the work.

Settings > Profile gets a "Pick on the map" row under the lat/lon fields
in the Advert location card. It opens the Map tab centred on the current
position, with a crosshair pinned over the map centre and a Cancel /
Save position bar. Pan and zoom as usual, then "Save position" saves the
centre as the advert position. Leaving the tab cancels it, and so does
Back on the M9, the pager and a Bluetooth keyboard. Auto-follow pauses
while picking so it cannot pull the centre away.

With GPS on and a fix, the GPS sets the position on every loop, so a
picked one would not stay: the row is hidden then, and a fix that
arrives while picking is reported instead of saved. A centre panned past
the antimeridian is wrapped to -180..180 before it is saved.

A node without GPS usually has no map view yet either, so it starts from
the first contact that has a position; only with none at all does it ask
for a rough location first. A typed but unsaved lat/lon is saved before
the jump. The bar sits above the zoom slider, so zooming still works
while picking, and left of the right-edge map buttons, so it never
covers them on a short screen.

The four new strings are translated in every deploy/apps/lang/*.lang
file and i18n_builtin.h is regenerated, so the i18n audit stays clean
for them.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-10 23:40:51 +02:00
Kaj SchittecatandClaude Opus 5.5 d68f276b06 touch: beta_93, contact nicknames, new control panel, tooltips, Pager shortcuts, speed and battery
Contact nicknames: TouchPrefsStore nick blob (48 x 38 B), display-only lookups
(contactShownName, threadShownName, senderShownName), Contact options >
Nickname on the shared text prompt (an empty OK clears it).
Control panel restyled in the launcher's tile language, with a Battery saver
tile; keyboard tooltips (navSetTip), an amber cursor and touch long-press
tooltips (the T-Deck touch read hid them on every poll).
T-LoRa Pager: M/C/A/S on Home, H/S tap and hold, Backspace = Back, N = jump to
new, held Enter = long press, per-tab focus memory, Power off confirm; LVGL
-O2 and IRAM, INT-gated keyboard, idle yield, status-bar setter guards, instant
tabs; PagerGps (u-blox software standby for rests, GPS rail cut when off),
panel sleep, amp off at boot, power-off parks the radio (NSS held), GPS, panel
and keyboard light.
Fixes: the DM duplicate check keys on the attempt and a path reset clears it,
the direct timeout masks the packed path byte, web remote hops (#636);
changelog over http (#638); Lua SDK send budgets and duplicate guard (#606);
GPS speed note (#613, CAP_GPS_BAUD_AUTO); emoji chat icon (#640); default
wallpaper (#626).
Heltec V4: Tetris, 2048 and Breakout unseeded and the built-in Lua stripped of
comments (identical bytecode), 27 KB freed; the baked language table is pinned
to beta_92's files (scripts/build/lang-pin-beta92) so it still fits.
Translations: 39 new keys in 14 languages, language store v24.
Site: theme, dark mode, aurora and guide refresh, SDK send limits, nicknames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:08:16 +02:00
CyrioandClaude Fable 5.1 9b891ff501 i18n: translate the 18 beta_90/91 strings missing from every .lang
scripts/build/i18n-audit.py reported the same 18 keys missing in all
14 language files -- the background picker (Background, Background
colour, Still/Moving aurora, Image, None, Choose an image, Choose the
colour, Set as background, the JPEG hints and the restart notice), the
GPS battery saver (Save battery, Check position every, the two
descriptions, "GPS: resting, next check in %lum%02lus") and the
"remote view open" touch-sleep block reason.

Each file gets the 18 rows in its alphabetical slot, translated rather
than left as English placeholders, reusing the terms the files already
use for the older wallpaper strings (Hintergrund, fond d'écran, sfondo,
achtergrond, háttérkép, pozadí, papel de parede/fundo, fundal, фон,
позадина, φόντο ...) and for the battery saver. "%lum%02lus" is kept
verbatim in every language. src/ui-touch/i18n_builtin.h regenerated
with scripts/build/gen-lang-builtin.py; the audit now passes for all
14 languages. "# ver: 23" left alone, since that is bumped at release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 07:05:06 -07:00
CyrioandClaude Fable 5.1 2f811a7a6b i18n: add "Clipboard empty" to every .lang (Fn+V toast)
Review follow-up: the new TR("Clipboard empty") toast had no row in
deploy/apps/lang/*.lang, so i18n-audit.py flagged it in all 14
languages. Added in each file's alphabetical slot (after "Client
repeat") and regenerated src/ui-touch/i18n_builtin.h with
scripts/build/gen-lang-builtin.py. The audit no longer lists the key.
"# ver: 23" is left alone, since that is bumped at release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 05:36:36 -07:00
Kaj SchittecatandClaude Opus 5.5 218bc6ed3e touch: beta_91, Home becomes widgets, backgrounds, GPS in the bar
Home is a grid of arrangeable widgets (24 kinds, presets, a layout per
orientation, Lua app widgets via wada.widget); Unread, Chat and Mentions
get a large size. Theme > Background puts an aurora (still by default, or
moving), a colour, a picture or nothing behind every screen, with glass
cards and a smoked-glass top bar. A GPS icon in the bar, a GPS battery
saver (u-blox GNSS stop/start where there is no power switch), and a
battery saver that no longer waits for Bluetooth and Wi-Fi to be off.
Also: navigation bar position, every settings page on the redesign's
cards, a faster app drawer, slower screensaver drift for burn-in, text
wrapping at spaces only, the V4-R8 Store Apps tab, and SDK Test marked
extended-SDK only (keeps the 2 MB V4 at 99.8%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:14:04 +02:00
Kaj SchittecatandClaude Opus 5.5 f1a7775d0c touch: beta_90, colour comes back
Testers said the colour was what set wadamesh apart, and the beta_89 redesign
took most of it out. This puts it back as choices on one new page.

Settings > Theme (CAT_THEME, Device group): a live preview drawn from a copy of
the palette (themeTokensFor), Night/Day cards, High contrast, the accent swatches
inline with Custom, MORE COLORS! with Regular/Neon/Pastel, Taste the rainbow, and
the chat looks. Mode, accent and look are pending until one Apply and restart on
the preview box; scrolled past it, a slim bar under the title carries the button
with six dots in the picked look. Chat icons, bubbles and compact apply live. The
look rows left Display; the control panel's theme chip long-press opens Theme.

The colour engine: ThemeTokens + lookAdjust over the mode and accent at boot
(applyBootTheme), section hues for the launcher, Settings, tab bar and status
icons, per-name colours (FNV hue) for names, glyphs, initials balls and bubble
tints, Neon lit edges and gradient primaries, Pastel tints, and a deeper Day ink
for names and glyphs on light lists. MORE COLORS! defaults on (Regular); HC and
e-paper keep their own colours. New prefs are standalone u8 keys (chat_icons,
col_more, col_style, col_rb), no schema bump.

Taste the rainbow: per-item hues round the wheel, a 10 Hz hue drift on the tab
marker, chats count and focus ring (rests when off, locked or screensaver), press
dip, an arrival flash on new bubbles (draw-only props, the virtual list never
sees it), rainbow lock mark, boot mark and Constellation nodes (CstTheme
node_sat/node_val), and a dot burst on enabling it.

Chat icons: Glyphs or Initials through one avatarApplyLook; AVATAR_SQUARE_FLAG
moved off LV_OBJ_FLAG_USER_1, which is the nav's NAV_SKIP_FLAG.

Size: wadamesh_mark_rgb.h was a static array included by main.cpp and
UITask.cpp, so every S3 image carried the 30 KB boot mark twice. One definition
now (UITask.cpp; the P4 never compiles main.cpp). The V4 goes from overflowing to
~30 KB free with all of the above.

Capture tour: wakes and ends the screensaver before shooting, DOC_LOOK forces a
look, DOC_LOOK_AUDIT walks the looks and the Theme page, DOC_MAP_SHOT waits for
Wi-Fi and for every tile before shooting the map.

25 new strings as English placeholders in every .lang file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:34:08 +02:00
Kaj SchittecatandClaude Opus 5.5 662665e6de touch: beta_89, the UI overhaul
Every screen redesigned to the Oct 2026 mockups, on one design system:
palette, type roles (generated Montserrat faces with Lucide line icons on the
LV_SYMBOL codepoints), a theme apply-cb that styles every LVGL widget, button
kinds, list rows, sheets as action lists, toasts and key-glyph avatars.

Rebuilt: status bar, Chats (filters, + and menu), conversation (route header,
hop meta, "Heard by N repeaters", delivery states, pill composer), lock screen
(mark, clock, 24 h activity, unread cards), Constellation screensaver, Home
launcher and launcher sheet, Status page, grouped Settings home, animated boot
splash, and the message glance as a notification card.

New: optional PIN lock (salted SHA-256, keyboard / touch pad / number pad per
board, backoff that survives reboot), screensaver on battery with its own
duration, T-Deck text sizes, trackball hold to switch Scroll and Cursor (Scroll
default, prefs v69/v70), hold Settings tab for the control panel. M9 bottom
notices removed. Every screen measured for the Large and Huge text sizes.

Fix: adverts over 2/3-byte path hashes never reached the recently heard table
(packed path_len compared as a byte count); ADVERT_PATH replies now carry the
whole multi-byte path.

English placeholders for the 204 untranslated keys in every .lang file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:26:41 +02:00
Kaj SchittecatandClaude Opus 5 a8f813c5f9 Merge PR #599 (oumike): battery saver, Condense Nav, Pager keyboard nav
Closes #591, #592, #593 and the M9 Bluetooth status-bar overlap museifu696
reported. Also the T-Pager battery saver zomgugoff asked for and the status-bar
clock fix.

Review notes, since this landed on top of a day of P4 and M9 work:

* The prefs change is done the right way. condense_nav is appended at the TAIL,
  CURRENT_VERSION goes 65 -> 68 with v66/v67/v68 each described, and the
  trailing static_assert moves to the new last field. v67 and v68 are one-time
  per-board resets with no new field, which is the pattern v63 and v65 used.
* Six conflicts, all in test_touch_prefs_schema.cpp, all the same shape: we had
  both repaired the same broken sums. His version is a superset (it adds
  condense_nav on top of the report_ping/report_done_n I added), so his wins.
  Verified afterwards that both sides survived rather than trusting the merge:
  the channel-send guard, the P4 tile-FS fix, statusPctOverflow, m9NoticeSet,
  m9DiscoveredUnseen and TDP4_C6_ID are all still present, as are his kbd_nav
  and Condense Nav work. Host suite 17 pass / 0 fail at v68.

Also here: PUBLISH the translations the PR added.

It added five strings to all fourteen language files with real translations, but
left "# ver: 22" alone and did not touch langs.json. Devices fetch lang/<ver>/,
so the version is the only thing that makes one re-download: without a bump the
work sits in the repo and reaches nobody, and every non-English device shows
those strings in English. The baked-in table was regenerated, so compiled-in
languages were fine -- it is the downloaded ones that would have silently
stayed behind. Bumped to 23, snapshotted into lang/23/, and langs.json points
at it. deploy-apps.sh validates: 14 languages and 14 apps, every referenced
file present.

That store still has to be pushed with the release; editing deploy/apps/ is not
publishing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 12:30:55 +02:00
Michael A. CojocariandClaude Opus 5.5 6d655b8301 feat(pager): keyboard navigation and remappable tab keys, as on the T-Deck (#591)
The wheel is the Pager's only way to move the highlight, which is slow
and imprecise. Bring the T-Deck's keyboard navigation to it:

- With no text field focused, programmable letters move the highlight
  (default I up, K down, J left, L right) and scroll (U / O). Select
  and Back default to no letter, since Enter and Backspace-hold (the
  wheel's click / long-press) already do them. On the Map the letters
  move the highlight; W/A/X/D stay the map pan.
- The tab keys are programmable like the T-Deck's, defaulting to the
  Pager's M/C/H/A/S, and the bar's labels show whatever is bound.
- Settings > Keyboard gets the same "Keyboard navigation" switch (on by
  default here) and the same tap-a-row-then-press-a-key remap rows; the
  T-Deck's rows move into settingsNavKeyRows() so both boards share them.
  The Pager's switch only turns the letters on or off; it never empties
  the focus group, which the wheel always needs.
- A remap refuses letters the Pager uses for fixed actions (Q/E slider
  nudge, W/A/X/D map pan; A stays allowed as a tab key).
- In wheel edit mode on a slider, J/L preview like a turn and the
  select key closes the edit, matching the wheel.

Settings: the Pager's kbd_nav and key bindings were never shown on that
board, so they held the T-Deck defaults. Schema v67 (no new field) puts
the Pager defaults in once on that board only. Two new strings,
translated in every deploy/apps/lang file; i18n_builtin.h regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 15:06:40 -04:00
Michael A. CojocariandClaude Opus 5.5 08750ab3a1 feat(ui): Condense Nav, a landscape nav rail with icon-only Home launchers (#592)
On short landscape panels (the T-Pager is 480x222) the bottom nav bar
spends a full-width strip on five icons. New Settings > Display switch
"Condense Nav", off by default, applies after a restart:

- The nav bar becomes a rail on the right edge (LV_DIR_RIGHT tabview).
  Pager cells show the key letter under the icon; the Tanmatsu keeps its
  coloured F-key shapes; the T-Deck / P4 key hints sit under the icons.
- Home's launcher column (Advert, Terminal, Files, Apps, Control) shows
  icons only; the buttons keep their height and spacing, and the status
  text, chart and info card widen into the freed space.
- The active-tab marker turns vertical beside the active cell, the
  unread and update badges move to the top-right of their cells, and the
  rail stays solid (and the marker visible) on the Map.
- Content that kept the bottom bar visible now stops at the rail
  instead: tabContentW() subtracts the rail, and the map canvas, Settings
  landing grid, chat-list rows, Mentions and the app drawer use it.
  Swipe-left on the rail opens the drawer (swipe-up on the bottom bar).

Portrait layouts and the M9 are unchanged: the mode is decided once in
buildUiTree from the pref and the orientation at boot, the switch is
greyed out in portrait and absent on the M9.

TABBAR_H becomes a runtime value (0 with the rail) next to a new
TABBAR_W. The pref is a new trailing field, condense_nav (schema v66);
the schema test's layout asserts now count the v64 fields they missed.
Strings added to every deploy/apps/lang file and i18n_builtin.h
regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 13:47:46 -04:00
Terry Carr 0b78e7a142 Add POI translations and implement map location features 2026-09-21 13:23:09 +01:00
Kaj SchittecatandClaude Opus 5 4df42eedf4 Merge PR #531 from oumike: device usability, input and accessibility
M9 Back and Bluetooth-keyboard Esc stop at the locked app-drawer root, M9
focus fixes, optional M5Stack CardKB on the V4 and V4-R8, V4-R8 touch and
text-size accessibility, Wio L2 SD retry, high-contrast day and night themes
(#544), channel region discovery (#541) and the Czech translation
contributed in #540 by brebtatv.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:54:09 +02:00
Michael A. Cojocari 0aa2f28c16 feat(ui): add high-contrast day and night themes 2026-09-20 19:08:45 -04:00
Kaj SchittecatandClaude Opus 5 80f6aa97ea apps: SD Scan 1.1, buttons above the findings list
An infected M9 card turned up with 373 findings. Every list row is a focus
stop on a keyboard board, and the firmware's key navigation collects at most
160 of them per screen, so in 1.0 the Remove all and Scan again buttons below
the list could not be reached on an M9 at all.

1.1 puts the buttons above the list, first in focus order, on the results
and failed-removal screens, and shows at most 60 rows plus an "and N more"
row (the title carries the full count). App-only: no firmware change.

Harness: a 373-finding scenario, and a check on every list screen that the
buttons come first and the focus stops stay under 160. It fails on 1.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 09:13:43 +02:00
Kaj SchittecatandClaude Opus 5 6868911f59 touch: SD Scan, find and remove Windows malware on the SD card
Some ThinkNode M9 cards shipped with a dormant Windows worm (Elecrow
security advisory, September 2026). An infected card seen since carries
autorun.inf in the root, launching xlfqf.pif on open, explore and autoplay
with random-junk comment lines in between: the Sality autorun pattern.

- SD Scan store app (deploy/apps/sdscan/1.0, requires "sd", not seeded):
  walks the card a small page per tick, lists what it finds and why, and
  removes it after a confirmation screen with Cancel first. It says on
  every screen that it only removes files it recognises and that
  formatting the card is the safe fix. On older firmware it still finds
  threats by name but cannot remove them.
- Firmware: wada.sd.check(path) and wada.sd.remove(path), plus paging for
  wada.sd.list(path, start, max) and caps().sd_clean. What counts as a
  threat lives in SdThreat.h: autorun.inf, Windows program, script and
  shortcut extensions, or a real MZ+PE header under any name. remove()
  classifies again in firmware and refuses anything else, so no app can
  use it to delete tiles, backups or chat history. It clears read-only,
  hidden and system first, because FAT refuses to delete a read-only file.
- A warning when a card with Windows malware in its top folder is mounted,
  at boot or on insert, offering SD Scan (or the Store).
- Tests: test/test_sd_threat.cpp, and SD Scan harness scenarios including
  the real infected card's root. Removal checked on a T-Deck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:33:30 +02:00
Kaj SchittecatandClaude Opus 5 ce37110515 touch: web unlock, a way out of console mode, paste keys, M9 update notice
- Web interface: unlock a manually locked screen (#506). The device publishes
  its lock state over the mirror socket and the page shows an Unlock button
  while it is set. The lock screen absorbs taps by design and only a held
  trackball or BOOT press unlocks on the device, so a browser had no way back
  from a screen it had locked itself.
- Console mode: hold the panel for three seconds to leave it (#507). The
  banner and `help` both say so. Console mode also applies the "Older keyboard
  protocol" setting now: that is applied in the graphical startup path, which
  console mode returns before, so the console ran on protocol detection alone.
  On a T-Deck that needs the older protocol every keystroke there is garbage,
  which is why `ui` could not be typed and the reporter had to side-load a
  second firmware to get the device back.
- Console mode: a touch wakes a dark panel again on the touch-only boards,
  where the keyboard and button wake paths sit below the console branch's
  return. The waking press is swallowed so it cannot also type.
- Paste into a key field lifts the key out of the surrounding text (#526): a
  32 hex digit channel secret or a 64 hex digit public key, spaced keys
  included. Before, the field filled with prose and the length cap cut the key
  off.
- ThinkNode M9: a waiting firmware update is visible (#443). The red "!" over
  the bottom-bar gear is built in the #else of that board's block, so the M9
  had no update signal at all; it gets a third slot in its own notice row,
  steady amber rather than blinking. The Settings tile in the app drawer
  carries an "!" on every board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:43:06 +02:00
Kaj SchittecatandClaude Opus 5 793a673c64 touch: Bluetooth LE keyboards on every S3 board
Bluetooth serves either the phone app or a keyboard (Settings > Bluetooth).
Pairing lists keyboards in pairing mode and pairs with or without a code;
the paired keyboard reconnects by itself.

Keys are read the way phones and computers read them: the keyboard's Report
Map says which report carries the keys and how, so media keys and touchpads
are left alone, and the boot protocol is only the fallback. Key positions are
translated with the chosen layout (US, UK, German, French, Belgian), with
AltGr and dead keys.

While a keyboard is connected:
- text goes into the focused field, and touchscreen-only boards keep the
  on-screen keyboard down (a second tap on a field brings it up anyway)
- the arrows, Tab, Enter and Page Up/Down drive the focus highlight
- Esc is the back button; a "Back key" setting picks another key for
  keyboards whose Esc key types a character
- tab hotkeys and their hints over the tab bar are switched on
- Command tapped alone opens the emoji picker in a chat
- the status bar shows a keyboard instead of the Bluetooth glyph

The Bluetooth page is now a flex column with the choice on top, and the
settings page refit keeps it scrollable when its content grows.

Settings: prefs v61 (mode, layout, paired keyboard) and v62 (Back key).
Tests: test/test_hid_report_map.cpp, test_touch_prefs_schema.cpp.
Not on the P4 boards (Tanmatsu, T-Display P4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:52:21 +02:00
Kaj SchittecatandClaude Opus 5 84f2ec02e2 i18n: publish the corrected language packs as v22
The "System Information" key fix (6bb98a1) changed the flat packs in
deploy/apps/lang/, but devices never download those: they fetch the immutable
copy at apps/lang/<ver>/<code>.lang, keyed on the version in langs.json. With
everything still at v21, the store kept serving the old key from lang/21/ and
no device re-downloaded anything, so the fix reached only the Heltec V4, which
bakes the table into its firmware.

Bumped "# ver:" and the catalog to 22 in all 13 languages and snapshotted the
packs into lang/22/, the documented three-step publish. The generator ignores
the version line, so the baked table and the published beta_80 firmware are
unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:42:30 +02:00
Kaj SchittecatandClaude Opus 5 6bb98a1224 i18n: fix the "System Information" key at its source (13 languages)
#458 renamed TR("System info") to TR("System Information") and updated the
translation table to match, so the title stayed translated. But
src/ui-touch/i18n_builtin.h is GENERATED ("DO NOT EDIT") by
scripts/build/gen-lang-builtin.py from deploy/apps/lang/*.lang, and the rename
never reached those packs. Every regeneration since then -- the PlatformIO pre-hook
and the IDF build.sh both run it -- quietly wrote the old key back, so the source
asked for "System Information" while all thirteen tables answered "System info",
and the page title fell back to English in every language.

That is also why #517 appeared to revert the rename: its copy of the header was a
freshly regenerated one, faithfully reproducing the stale packs.

Rekeyed the thirteen source packs, then regenerated. The generated table changed by
exactly one key per language and nothing else, which confirms it was otherwise in
step with the packs, and the fix now survives the build's own regeneration, which is
the thing that used to undo it. The corrected packs reach devices that download a
language once the app store is republished with the next beta.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:56:25 +02:00
Michael A. Cojocari f3136ea951 Merge branch 'main' into feat/tdeck-pro-target
# Conflicts:
#	deploy/apps/lang/bg.lang
#	deploy/apps/lang/de.lang
#	deploy/apps/lang/el.lang
#	deploy/apps/lang/es.lang
#	deploy/apps/lang/fr.lang
#	deploy/apps/lang/hu.lang
#	deploy/apps/lang/it.lang
#	deploy/apps/lang/nl.lang
#	deploy/apps/lang/pt-br.lang
#	deploy/apps/lang/ro.lang
#	deploy/apps/lang/ru.lang
#	deploy/apps/lang/sr.lang
#	deploy/apps/lang/uk.lang
#	src/ui-touch/UITask.cpp
2026-09-08 15:41:45 -04:00
Kaj SchittecatandClaude Opus 5 6160184531 Translation placeholders for the strings from oumike's seven PRs
Five new user-facing strings arrived with #429/#439/#440/#441/#442/#444/#445.

All nine S3 envs and both ESP32-P4 targets green on the merged result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:35:56 +02:00
Kaj SchittecatandClaude Opus 5 96ba457984 Fix a Wi-Fi callback race that crashed at boot, and make the GPS privacy setting reachable
Two things, both found by looking rather than by being told.

A Heltec V4-R8 crash dump from beta_77 decodes to a data race in the Arduino
core's Wi-Fi event list. Correcting for the unwinder mangling Xtensa return
addresses, the event task was inside _eventCallback copying a WiFiEventCbList
entry, iterating the callback vector, while loopTask was still in setup() inside
_M_realloc_insert growing that same vector through WiFi.onEvent(). The
reallocation frees the buffer the event task is walking, so it then calls
through a dangling function pointer, which is the garbage program counter in the
dump.

Neither side locks, and the vector is the core's, not ours. What is ours is the
ordering: both handlers were registered AFTER the stack was started, one of them
after WiFi.begin(), so a STA_DISCONNECTED arriving during boot association lands
in the window. A failed first association is ordinary, which is why this happens
at all. Both now register before WiFi.mode(WIFI_STA), so nothing mutates the
vector once events can flow and the race is impossible rather than unlikely.
This is a crash during setup(), so on an unlucky unit it is a bootloop, which
makes it worth more than its single report.

The moved handler drops its WIFI_DEBUG_PRINTLN tracing, which comes from a
header not in scope that early. The prints compile to nothing in a release build
and the reconnect flag they accompanied is unchanged.

Separately: the location-privacy setting shipped in beta_77 on the wrong page.
It was inside the Sensors section rather than GPS, because the insertion was
anchored to a comment belonging to the Sensors block, and that section is
described in its own code as V4-with-kit only. So on most boards it was not
reachable at all, which is exactly what two people reported: they went looking
and correctly concluded it was not there. It now sits in the GPS section, is
named "Location privacy" rather than "Position in adverts" (nobody searching for
a privacy control scans for the word "adverts"), and offers four buttons instead
of a cycling one, so every option and the current choice are visible without
touching anything. Given the failure here was discoverability, showing the most
without interaction is the point.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:17:50 +02:00
Kaj SchittecatandClaude Opus 5 3713b11829 Download mode now says how to leave it (#104)
dreirund reported the device as frozen at "Download mode... reflash over USB".
It was not frozen: that is the state working as designed, waiting to be flashed
with its own UI stopped. They accepted that once told, and made the fair point
that the button should say what it will do.

So the menu entry now reads "Download mode (wait for USB flash)", and the
message shown before the UI stops says that RESET cancels, which is the piece of
information whose absence turned a working feature into a bug report. It also
stays on screen long enough to be read.

A state a user cannot leave without already knowing the way out is a bug in the
telling, even when the code is right.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 18:06:54 +02:00
Kaj SchittecatandClaude Opus 5 7bcaacfb93 Translation placeholders for the file-transfer strings
Twelve new user-facing strings arrived with #413. Added as placeholder rows so
the audit is clean and translators have the keys to fill in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 11:30:04 +02:00
Kaj Schittecat d4a430c03e Merge PR #409 (oumike): Wio Tracker L2 wake and screen controls
Conflict with #407 resolved in favour of the newer gesture: #407 changed the M9
unlock from a hold to a double-press, and #409 branched before that and still
described the hold. The M9 strings keep #407's wording; #409's Wio Tracker L2
arm is taken as written, since that board's gesture is not what changed.
2026-09-06 10:54:53 +02:00
Kaj Schittecat 0719834cc1 Merge PR #407 (oumike) 2026-09-06 10:54:31 +02:00
Kaj SchittecatandClaude Opus 5 993e147f3e Set the clock by hand, for devices with no GPS, network or phone (#105)
Requested by dreirund: with no GPS fix, no Wi-Fi and no companion app there was
no way to set the clock at all. That is not cosmetic, because message timestamps
are built from it, so a device used purely offline could not hold a correct one.

Settings, Clock now takes local time as "YYYY-MM-DD HH:MM". mktime() reads it
through the configured zone, the same zone the clock is displayed in, so what you
type is what you see rather than something you convert to UTC yourself, and
tm_isdst is left at -1 so summer time is not an hour out. The field is prefilled
with the current reading, which makes a correction an edit of a digit or two
rather than typing a full stamp on a device keyboard.

Rejected values are refused the same way every other clock source is: below the
send-timestamp floor is not accepted, so a mistyped year cannot walk the ratchet
backwards.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 10:11:31 +02:00
Kaj SchittecatandClaude Opus 5 ee15fcb335 Fix two beta_76 regressions of mine, and advertise a position without your address
#415: a Lua app was covered by the app drawer whenever a message arrived.
Reported by jadestarwatcher, reproduced by mysterywavi, both on T-Deck. Mine,
from the #393 badge fix. Apps launched from the drawer deliberately leave it
alive underneath (appTileCb says so outright), and my badge refresh rebuilt it on
every unread-count change, with openAppDrawer() ending in a move_foreground that
threw it on top of the running app.

The refresh now runs only when nothing is drawn in front of the drawer, tested by
sibling order rather than by listing the tools, so a tool added later is covered
without anyone remembering to update a list. The status bar is excluded, since it
legitimately floats above the drawer at all times and treating it as covering
would have stopped the badges refreshing at all. Leaving the signature stale is
what makes it self-healing: the check runs again each tick, so the count is right
by the time the drawer is back in front, with no need to hook every close path.
Scroll position is now preserved across the rebuild too, which my change had also
been resetting.

#410: on the M9 the accent variants appeared but could not be selected. Also mine,
from the #387 work. The picker was handled below m9HandleArrowKey, which takes
LEFT and RIGHT for the caret and returns, so the arrows never reached it: the box
was drawn and nothing could touch it. Lifted above that call, as its own function
rather than a second copy of the logic.

#399, requested by @Danie10: an option to advertise a position near you rather
than your address. Settings, GPS, cycling exact / 100 m / 250 m / 1 km.

The displacement is derived from the node identity, so it is the same offset
every time. That is the whole point rather than an implementation detail: a fresh
random offset per advert would scatter points around the true position, and
averaging a night of them would recover the centre exactly. A fixed displacement
instead looks like a node that sits somewhere else, which is what a manually set
location already looks like. It applies only to our own adverts; the map and the
GPS page keep the real fix, because the aim is to tell other people less, not to
lie to yourself.

Schema v57. The host test caught the new field the moment it was added, which is
what it is for.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 09:57:25 +02:00
Michael A. Cojocari 3a2781bb6e Work 2026-09-04 21:04:44 -04:00
Michael A. Cojocari ce6ec9e3a8 Fix Wio Tracker L2 wake and screen controls (#406)
Drive the L2 I2C backlight during sleep and wake, poll its expander button reliably, and remove irreversible software power-off. Make a two-second wake-button hold lock or unlock, and keep Home and empty-map chrome clear and readable.

Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
2026-09-04 14:25:33 -04:00
Michael A. Cojocari 420e13cafb Improve M9 message actions and unlock gesture (#402)
Open the selected message action menu with a normal Enter press. Replace the lock-screen long wait with a d-pad-center double press while preserving directional behavior and the controller long-press fallback.

Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
2026-09-04 14:00:17 -04:00
Kaj SchittecatandClaude Opus 5 2a8b62c6a7 Merge PR #375: Day and Night themes (#296)
oumike. A firmware-wide appearance setting: Night stays the default and renders
exactly as before, Day is a low-glare light palette with its own semantic roles
for text, panels, fields, borders, controls, focus, charts, status colours and
chat surfaces. Selecting a mode restarts, so every LVGL object is rebuilt from
one coherent palette rather than half-repainted. There is a Night/Day selector
in Settings, Display, the Control Center Theme chip is now a direct toggle whose
sun/moon icon shows the active mode, and the standalone console UI has a
matching Day palette.

Main already carried the palette scaffolding for this, pinned to Night behind a
comment saying it was waiting on this branch, so the merge mostly replaces those
stubs with the real thing.

The prefs schema needed care. The branch appended theme_mode as v54, but v54 and
v55 were taken by boot_wifi_time/boot_wifi_open and loud_alerts before this
merged, so the field moves to the tail behind them and the version becomes v56,
with the trailing-field assert and the migration step moved to match. A packed
struct read back at the wrong offsets is the failure this schema's asserts exist
to prevent, so the invariants from both sides are kept rather than one replacing
the other.

That also surfaced a stale test: the v53 case asserted that v53 plus exactly two
bytes was the whole struct, which stopped being true when loud_alerts landed and
was not caught because the host test was not run then. It now counts every byte
appended since and asserts the new fields come back at their defaults. The
schema host tests pass. While there, the size comment said "about 500 bytes";
the struct is 114.

Built on all nine S3 envs and both ESP32-P4 targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 09:11:54 +02:00
Kaj SchittecatandClaude Opus 5 12384c97b7 M9 panel sleep, Discovered hop counts, and hiding dead keyboard controls
The M9's screen was never really going off (#390, Buko84, who spotted the lock
screen still faintly visible in bright light). Backlight off is not screen off:
the ST7789 keeps refreshing the same static image behind a dark backlight, which
is exactly what retains an image into the glass. The panel-sleep command the
T-Deck and V4 use was a no-op stub on this board and the M9's backlight path
never called it anyway. Both halves are fixed. The M9 takes the display driver's
default constructor branch, on the global SPI instance it shares with the radio
and with no PIN_TFT_SCL/SDA defined, so the command goes over that same bus
object inside a transaction rather than a second one on pins this board does not
declare.

The Discovered list showed impossible hop counts (#394, jesshampshire). 0xFF is
OUT_PATH_UNKNOWN, meaning an advert arrived by flood with no known path, and it
was being printed as a number: "255 hop". It now says the path is unknown, says
"direct" for zero hops, and pluralises, so a two-hop node no longer reads as
"2 hop".

Keyboard backlight controls are hidden on the older T-Decks (#382,
jesshampshire), which have no controllable backlight, so those controls did
nothing while still looking like settings. The "Older keyboard protocol" switch
added in beta_74 already declares which keyboard is fitted, so it gates these
too rather than asking the same question twice. The other half of that request,
swallowing the backlight key combo so it does not type a stray character, needs
to know which character actually appears; asked on the issue.

Also removed a duplicated pair of declarations in the prefs header.

Built on all nine S3 envs and both ESP32-P4 targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 01:09:53 +02:00
Kaj SchittecatandClaude Opus 5 3b9c2298ba Louder notification chime on buzzer boards (#388)
Buko84 asked whether the M9's notification sounds can be made louder. There is
no volume setting because there is nothing to turn: the buzzer on these boards
is a bare piezo on a GPIO driven by tone(), a fixed-duty square wave, so
amplitude is simply whatever the part does at that pitch.

Frequency is the lever that does exist. A piezo is far louder near its mechanical
resonance, typically around 4 kHz, than at the 1 to 2.6 kHz the chime uses, so
"Loud alerts" in Settings, Sound plays the same three-note shape shifted into
that band. Same chime, so it still reads as the same alert rather than a new one,
and toggling it plays it so the difference is audible while the switch is still
under your finger.

Off by default and opt-in, because the exact resonant peak varies by part and
this has not been measured on an M9. Schema v55, field appended at the tail.

Built on all nine S3 envs and both ESP32-P4 targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 00:52:38 +02:00
Kaj SchittecatandClaude Opus 5 102c239c69 Four reported issues: P4 Wi-Fi on after power cycle, plus three UI fixes
Wi-Fi came back on after a power cycle on the T-Display P4 while every switch
in the UI read off, and the radio really was on: it downloaded a map tile
(#373, wb6zsu, with photographs of both states). The C6 runs its own AT
firmware with auto-connect enabled, so it rejoins the last access point by
itself at power-on, before the firmware ever asks it to. The loop that enforces
the radio pref only acts on a change, and on the first pass it recorded the
state instead of applying it, so "off" never became a transition and the join
the C6 made on its own was never torn down. That is also why toggling Wi-Fi on
and then off fixed it: that made a transition. The state is now applied on the
first pass, once the AT link is actually up. The Tanmatsu carried the same
first-pass logic and got the same fix.

The map's own location marker is a bare white glyph and vanishes on light
basemaps (#366, 100monkeys). The contact markers a few lines below it already
carry a dark border for exactly this reason and self never got one, so it now
gets the same: a dark chip behind the glyph, which reads on any basemap without
spending a colour that already means something else.

Wi-Fi passwords can be revealed while typing (#381, jesshampshire). Entering a
PSK blind on a device keyboard is a real failure mode: the reporter needed about
ten attempts to join their own network. The toggle sits on the password label's
line so it costs no vertical space, and it switches the keyboard mirror too, or
the characters stay masked in the one field being looked at.

The composer shows a character count (#350, jrote1). The 160-character cap was
silent: typing just stops, which reads as a broken keyboard. It appears only in
the last quarter and marks the limit when reached, so an ordinary short message
carries no readout. Counted in codepoints to match how the cap is enforced;
by bytes an emoji would read as four characters.

Built on all nine S3 envs and both ESP32-P4 targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 00:16:39 +02:00
Kaj SchittecatandClaude Opus 5 c625d38244 Merge #384 (DM timestamps), #385 (M9 battery), #386 (hardware RTC), and fix the P4 link
oumike's #384 closes #374: MeshCore seeds _txt_last_ts from an unrestricted
random uint32 and sendMessage() then raises the real RTC timestamp above it, so
outbound direct messages carried future dates on most boots. Patched in the
vendored core, fail-closed and idempotent.

#386 closes #383: hardware RTC for the Pager's PCF85063A and the M9's PCF8563
with validation on both sides, time retained through a full power-off, and an
opt-in cold-boot Wi-Fi time sync. Prefs schema to v54, appended at the tail with
the assert updated.

cvhviz's #385 makes the M9's battery saver reachable at all (the machinery was
wired but the only enable switch was compiled for two other boards), fixes
GPS-off leakage, and enlarges the GPS UART ring mid-session.

Two build problems, neither visible to the authors:

  - #384 wires its core patch into the IDF builds as a fail-closed VERIFY, and
    the vendored copy in this checkout predated it. Applying the same patch step
    fixes it; the error already said to run fetch-deps.sh, which is the right
    design.
  - #385 calls gpsEnsureBigRxRing() from UITask.cpp and MyMesh.cpp, which every
    target compiles, while the definition is in src/main.cpp, which the two
    ESP32-P4 targets never compile. Both P4 builds failed to link. A no-op
    definition for those boards lives in UITask.cpp now, which is the standing
    rule for anything shared between the UI and the Arduino main. A no-op is
    honest here: neither P4 drives GPS through Serial1.

Built on all eight S3 envs and both ESP32-P4 targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 23:28:37 +02:00
Kaj SchittecatandClaude Opus 5 d33e5dec31 Merge cvhviz's M9 fix series (#354, #357, #358, #359, #360, #365)
The one that matters most is #354: tileCacheRemove() was written for the
firmware's own LittleFS cache but resolves through s_tile_fs, which is
re-pointed at the SD card root on every board that caches tiles there. So it
was calling SD.remove() on the user's own offline map pack, permanently, for
every queued tile whose .jpg happened to be absent. Gated to the internal
partition now.

The rest: remount through the full ladder after an in-device format (#359),
live labels no longer overlap the rows beneath (#357), hidden and inert widgets
stay out of the d-pad focus group (#358), keypress work is attributed in the
stall ring (#360), and GPS acquisition is measured from when it started rather
than when you looked (#365).

Built on all eight S3 envs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 10:35:04 +02:00
Michael A. Cojocari 952075a22e Day/Night 2026-08-31 09:37:45 -04:00
Kaj SchittecatandClaude Opus 5 41c7b94a1d keyboard: a way out of a wrong protocol guess that does not need the keyboard (#351)
Vybo reports no change in beta_73. The runtime demotion I added is not firing on
his device, and I still do not have the one serial line that would say why. So
rather than guess a third time, give him a switch.

Settings > Keyboard > "Older keyboard protocol" skips raw detection entirely and
uses the protocol every T-Deck controller understands. The point is that it is
reachable BY TOUCH: when detection guesses wrong the keyboard types the wrong
letters, so any remedy that needs the keyboard is not a remedy. It applies
immediately, without a reboot, because the driver re-reads the flag every poll
and will drop out of raw mode on the spot.

The cost of turning it on is modifier latching, which a controller that needs
this switch cannot do anyway.

Schema goes to v53, appended at the tail with the existing static_assert
updated to point at the new last field.

This does not replace finding the real cause; it stops people being stuck while
I do. Automatic detection is unchanged and still the default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:36:42 +02:00
Kaj SchittecatandClaude Opus 5 fff611692f touch: show the hop hash beside the repeater name in a route (#348, #347)
jrote1: a route lists resolved repeater names, and when a name is wrong or two
repeaters share one there is nothing left to identify the hop by. The hash is
what the node is actually keyed by on the wire, so it is the part that stays
true exactly when you are reading a route to work out where a message went.

Each hop now reads "3A Repeater Name" rather than just the name. The unresolved
case is unchanged, since it was already showing the hash.

Also pisti87's two corrections (#347): "Known regions" had never been
translated, and the Bluetooth pairing hint gets his shorter wording. Languages
to v21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:37:24 +02:00
Kaj SchittecatandClaude Opus 5 afecfc0224 i18n: two Hungarian strings pisti87 asked for (#340)
"Mark read" was an untranslated placeholder. "Join the public channel" had the
long form, which he wants shortened to fit the button.

His text for the second reads "nyílvános"; the file already uses "nyilvános"
everywhere else, so this follows the file rather than introducing a second
spelling. Easy to change back if the long i was deliberate.

The other line numbers in that issue are from his own build and no longer point
at what he means, so they need his source rather than mine to resolve.

Languages go to v20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:38:22 +02:00
Michael A. CojocariandClaude Opus 5 45447caa7f touch: hand a contact to the node next to you (#321)
The contact action sheet could message, ping, telemetry, range-test, favorite,
share location, reset path, block and delete — but not hand the contact itself
to anyone. The plumbing was already there and only the companion protocol could
reach it: shareContactZeroHop() has backed CMD_SHARE_CONTACT all along.

So this is a button and a wrapper. uiShareContact() sits next to
uiResetContactPath() and looks the contact up in the live table the same way;
actionSheetShareContactCb mirrors actionSheetResetPathCb exactly — fetch by
index, close the sheet before acting, toast the result. No refreshContactsList():
unlike Reset path and Delete this does not touch the contact table, it only puts
a packet on the air.

Zero-hop, deliberately. Flooding a third party's advert spends the whole mesh's
airtime on a packet nobody asked for, and it re-advertises someone else's node
well beyond the room you are standing in. Zero-hop is what the companion command
already does and keeps the action to "hand this to whoever is next to me". A
flood variant is a different airtime trade-off and does not ride along with this.

Gated on !from_map like Share my loc and Block, so the map-marker sheet stays
compact, and placed after Reset path so Block and the full-width Delete keep the
bottom of the sheet.

grid_items goes 6 -> 7 for the non-from_map case. That is housekeeping, not the
fix — as the #306 note at the bottom of the function says, scrollability is
decided from the height the buttons actually reached, and that is untouched. The
#266 Share my loc row is still missing from the same tally; fixing it here would
widen the diff into behaviour this issue put out of scope.

Three new strings across the thirteen language files (Contact gone already
existed), inserted in sorted position and translated in each file's own style for
its neighbours — Contact added for the success toast, Send failed / Save failed
for the failure one, Share QR / Share channel for the button. i18n_builtin.h is
regenerated, not hand-edited; it costs ~2 KB of .rodata, which the V4 feels most.

Audit clean on all thirteen (1034 -> 1037 keys). T-Deck and Heltec V4 TFT build.
Not yet checked on hardware: a second node in direct range actually adding the
contact, and the smallest panel at the largest UI scale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:24:32 -04:00
Kaj SchittecatandClaude Opus 5 799756cd94 i18n: the signal card's own strings were never wrapped
pisti87 pointed at UITask.cpp:22021 twice, and he was right both times. sigCell()
does not translate its argument, so "Signal", "Signal (stale)" and "nothing heard
yet" were raw at the call site and the card read English in every language.

Easy to miss because the home-graph signal popup has its own TR()'d copies of the
same three strings, so the text is translated in one place and not the other. It
also explains why "nothing heard yet" matched no key when his translation for it
did not appear: there was no key, only a literal.

Hungarian supplied by him.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:41:31 +02:00
Kaj SchittecatandClaude Opus 5 2475cba5e7 store: do not offer an app the board cannot run
beta_69 published Wardrive and Nearby. Both need the extended SDK, and the
Heltec V4 does not have it, so the most common board in the mesh showed a
prominent Get button for two apps that download, install, open, and then refuse
to do anything. matthewjk reported exactly that on Discord and had to be told it
was a hardware limit.

An app can now declare `"requires"` in the catalog. The Store greys the button,
labels it N/A and says so on the row, rather than letting someone spend a slow
download to find out. Wardrive and Nearby declare sdk_ext.

An unknown requirement allows the install: a newer catalog must not disable apps
on firmware that has never heard of the capability name, which would be exactly
the wrong way round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 10:54:24 +02:00
Kaj SchittecatandClaude Opus 5 cecede23be i18n: undo the keys my extractor invented, and stop it inventing more
pisti87 read v18 and called it chaos. He was right, and it was mine.

Two defects in the extractor I added yesterday:

  - It joined every string literal in a call argument, so a ternary became one
    key. `cut ? "Paste (move)" : "Paste (copy)"` shipped as the single key
    "Paste (move)Paste (copy)", and with it "Unblock  Block",
    "Unfav  Favorite", "Other networksNetworks", "Batteryactivityon" and
    "Stop sharing loc  Share my loc". None of those strings exist anywhere in
    the firmware. Literals are now grouped only when genuinely adjacent, which
    is what the compiler concatenates, so both branches become their own key.

  - It scanned raw source, comments included. That is how "Geblokkeerde
    gebruikers" -- Dutch, appearing only inside a comment about how a long
    translation degrades -- became a KEY in the Hungarian file. Comments are
    stripped now, string literals preserved.

202 invented rows removed across the thirteen files. Only rows that were both
unknown to the extractor AND still untranslated were touched, so no
translator's work could be lost either way.

Also his: the curly quotes in the Hungarian credits render as boxes because no
bundled font carries U+201E/U+201D. Five values de-curled. And five strings he
found raw are wrapped: the Wi-Fi rescan and hidden-network rows, the update
check, and the downgrade prompt. The two Wi-Fi rows build their label at
runtime, since TR() returns a pointer and cannot join a glyph literal at
compile time.

Languages go to v19. v18 is deleted rather than left behind: it was only ever
correct for about a day and everything in it is superseded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 10:23:09 +02:00
Kaj SchittecatandClaude Opus 5 c044f75d11 i18n: pisti87's Hungarian for the strings the audit could not see (#294, #277)
33 of the pairs he posted apply directly to keys that only became visible when
the extractor learned about helper-wrapped and table-held literals. The rest
did not, and the reasons are worth recording rather than dropping:

  - Four were escaped in his markdown, so the key read `Node\\nRegion` against a
    real key of `Node\nRegion`. Matched after unescaping.
  - `Export crash report` is `Export crash report (%uK)` in source; the size is
    part of the label. Applied with the conversion appended.
  - `Distance` and `Heard` are column headers he read off the screen; the keys
    are `Distance: km` and `Recently heard`. Not guessed.
  - `Sent only when that contact asks...` is a translation of wording the
    English has since changed. Applying it would ship a Hungarian sentence that
    no longer describes what the setting does.
  - `nothing heard yet` and `Other (hidden) network...` match no key at all,
    which usually means a raw string somewhere the audit still cannot see.

88 Hungarian rows are still English: the remainder of the newly visible keys,
plus console mode, which is new.

Languages go to v18. The v17 snapshot is deleted rather than kept: it was cut
before the audit fix added 116 keys, so it was already wrong, and it never
reached a device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:06:11 +02:00
Kaj SchittecatandClaude Opus 5 eb439c8baa i18n: find the strings the audit could not see, and the apps it never checked
pisti87 reported a long list of text that stays English whatever the language,
and said the strings were in his language file and still did not appear (#257).
Both halves are true, and the reason is the audit.

The extractor only ever recognised TR("literal"). Three very common shapes were
therefore invisible:

  mk_row_btn("Reload tiles in view", cb)      // helper TR()s its parameter
  for (auto& r : rows) TR(r.label)            // literal lives in a local table
  TR(contactsSortOptName(m))                  // helper returns one of several

All three translate correctly at runtime, so the source looks properly wrapped.
But the literal at the call site was never emitted as a key, so it never entered
a .lang file, so no translator could ever supply it -- and adding it by hand
did nothing, because the audit's key list is what the files are checked against.
That is 51 strings across the map options sheet, the sort sheets, the contacts
filters and the home launcher.

The audit now understands all three, plus tr("...") in the Lua apps, and the
newly visible keys are in all thirteen files as placeholders so translators can
see them. 1017 keys, up from 966.

Four strings were genuinely raw and are now wrapped: the reader's idle status,
the Discover empty feed, the crash-report export button and Paste (move/copy).

Lua apps had no way to translate anything at all, so every built-in was hard
English regardless of the device language. wada.sys.tr() gives them the same
table the interface uses; airtime 1.4 is the first to use it, with the
`sys.tr or identity` fallback so it still runs on older firmware.

Two more instances of the drift this issue is really about:

  - gen-lua-builtin.py read out/firmware/apps/, which nothing writes -- the
    deploy rsyncs deploy/apps/ straight to the VPS. So the mirror was stale and
    the two apps added in beta_68 were never baked in: boards that cannot reach
    the Store shipped without them. It reads the canonical directory now, and
    regenerates from the same pre-build hook as the language table.
  - Baking a row whose translation equals its key does nothing, since TR()
    returns the key on a miss. Skipping them takes the header from 1.11 MB to
    939 KB and gives the V4 back 16 KB of flash, which matters at 89%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:29:05 +02:00