Build each screen in one language: the screen texts from the translations, European letters in the fonts, and Language & region in the app

The firmware takes its texts from screen_manager/translations/<code>.json at build time (components/smart_display:
one table in main.cpp, keys in screen_text_keys.h), and the fonts carry every letter European languages write with.
The app keeps Settings -> Language & region (language, clock, numbers) for every screen, writes LANGUAGE into the
screens' YAML and sends the clock and the number format in the layout message; the per-screen 24-hour clock goes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-19 14:24:19 +02:00
parent 9362a5f1db
commit 378d0b3e61
51 changed files with 2321 additions and 383 deletions
+313
View File
@@ -0,0 +1,313 @@
// GENERATED by tools/i18n.py (`python3 tools/i18n.py header`): the English texts for the C++ tests, which
// build a header without ESPHome and so without the table a screen's build writes into main.cpp.
#pragma once
#include "components/smart_display/screen_text.h"
static_assert(screen_text::KEYS_HASH == 0x725A92EEu && screen_text::KEY_COUNT == 302,
"screen_text_keys.h does not match screen_manager/translations/en.json: run tools/i18n.py header");
const char *const screen_text::TABLE[] = {
".",
",",
"Just now",
"{n} min ago",
"1 hour ago | {n} hours ago",
"Yesterday",
"1 day ago | {n} days ago",
"1 week ago | {n} weeks ago",
"1 month ago | {n} months ago",
"1 year ago | {n} years ago",
"In {n} min",
"In 1 hour | In {n} hours",
"Tomorrow",
"In 1 day | In {n} days",
"AM",
"PM",
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Su",
"Mo",
"Tu",
"We",
"Th",
"Fr",
"Sa",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
"{weekday} {day} {month}",
"{weekday} {day} {month}",
"{day} {month}",
"{weekday} {time}",
"{n} s",
"{n} min",
"{n} h",
"{h} h {m} min",
"{n} d",
"{d} d {h} h",
"Connecting to Home Assistant",
"Waiting for ESP Screens",
"Loading tiles...",
"HA not connected",
"ESP Screens not active",
"Choose tiles in HA",
"Home",
"No network",
"Settings",
"Brightness",
"Night",
"Screen",
"This screen",
"Dark mode",
"Auto standby",
"Standby after",
"Standby brightness",
"Night mode",
"Starts",
"Ends",
"Night brightness",
"Back to page 1",
"After",
"Also on standby",
"Swipe between pages",
"Page buttons",
"Rotation",
"Address",
"Firmware",
"Home Assistant",
"Restart",
"Tap again to restart",
"{n} sec",
"{n} min",
"{n} h",
"{h} h {m}",
"Tap to open",
"Page {n}",
"Unavailable",
"Refused",
"Command sent...",
"Confirmed by Home Assistant",
"Notification",
"OK",
"Color",
"Color temperature",
"Brightness",
"Waiting...",
"Effect",
"Nothing to choose",
"Loading...",
"Not connected",
"Target",
"Mode",
"Fan",
"Swing",
"Now {value}",
"Check the robot in HA",
"Automatic suction power",
"The robot chooses suction and water",
"Suction and water as set per room",
"Quiet",
"Normal",
"Turbo",
"Max",
"Pause cleaning",
"Pause",
"Resume",
"Start cleaning",
"Clean",
"Dock",
"Position",
"Tilt",
"Tilt {n}%",
"Feels like {n}\302\260",
"No daily forecast from Home Assistant",
"Coming days",
"No data",
"average",
"average of {duration}",
"Now",
"1 hour",
"24 hours",
"1 week",
"High {value}",
"Low {value}",
"once | {n} times",
"Loading history...",
"No history in this period",
"No history available",
"Muted",
"Not playing",
"Loading",
"Running",
"Paused",
"Stopped",
"Pause",
"Start",
"Cancel",
"Paused {time}",
"Sun",
"Sunrise {time}",
"Sunset {time}",
"rise {time}",
"set {time}",
"Running...",
"Never run",
"Last {time}",
"Yesterday {time}",
"Last {date}",
"No image from this camera",
"Loading image",
"Live",
"Recording",
"Tap to view",
"No image yet",
"Activate",
"Run",
"Press",
"Follow the calibration wizard",
"GT911 touch check",
"Hold each tap for a moment",
"Tap the crosshair\012Point {point}/5 - tap {tap}/3",
"Tap gently, a bit longer",
"Tap the same crosshair 3 times",
"Measurement rejected; try again",
"Save failed; try again",
"On",
"Off",
"Off",
"Heat",
"Cool",
"Heat/Cool",
"Auto",
"Dry",
"Fan only",
"Heating",
"Cooling",
"Idle",
"Off",
"Drying",
"Fan",
"Preheating",
"Defrosting",
"Open",
"Closed",
"Opening",
"Closing",
"Playing",
"Paused",
"Idle",
"Standby",
"Home",
"Away",
"Above the horizon",
"Below the horizon",
"Docked",
"Cleaning",
"Paused",
"Returning to dock",
"Idle",
"Sunny",
"Clear, night",
"Cloudy",
"Partly cloudy",
"Rainy",
"Pouring",
"Snowy",
"Snowy, rainy",
"Fog",
"Hail",
"Lightning",
"Lightning, rainy",
"Windy",
"Exceptional",
"Low",
"Normal",
"Charging",
"Not charging",
"Danger",
"Safe",
"Cold",
"Normal",
"Connected",
"Disconnected",
"Open",
"Closed",
"Open",
"Closed",
"Danger",
"Safe",
"Hot",
"Normal",
"Light",
"Dark",
"Open",
"Locked",
"Wet",
"Dry",
"Motion",
"No motion",
"Moving",
"Still",
"Occupied",
"Clear",
"Open",
"Closed",
"Plugged in",
"Unplugged",
"On",
"Off",
"Home",
"Away",
"Problem",
"OK",
"Active",
"Inactive",
"Unsafe",
"Safe",
"Smoke",
"No smoke",
"Sound",
"Silent",
"Tampering",
"OK",
"Update",
"Up to date",
"Vibration",
"Still",
"Open",
"Closed"
};
const char *const screen_text::LANGUAGE = "en";
int screen_text::plural_index(int n) { return n == 1 ? 0 : 1; }
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "components/smart_display/runtime_model.h"
#include "components/smart_display/cyd_ui.h"
#include <cassert>
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#define THEME_TEST
#include "components/smart_display/alert_overlay.h"
#include <cassert>
+12 -2
View File
@@ -4,6 +4,7 @@ The screen maps the device class itself (`tile_controls::binary_state_text`), fr
state message has carried since app 0.2.23: nothing new on the wire, so it works with older apps too. These tests keep
its table equal to the add-on's `header_bar.BINARY_STATES`, which the top bar and the history card use.
"""
import json
import re
import sys
import unittest
@@ -22,9 +23,17 @@ TILES = (ROOT / 'components/smart_display/runtime_tiles.h').read_text()
PROFILES = ('guition-4848s040.yaml', 'home-like-2432s028.yaml', 'packages/cyd.yaml', 'packages/guition.yaml')
ENGLISH = json.loads((ROOT / 'screen_manager/translations/en.json').read_text(encoding='utf-8'))
def firmware_words():
"""The firmware's table (app 0.2.90: keys into screen.ha.binary, in the screen's language) in English."""
block = re.search(r'constexpr BinaryWords BINARY_WORDS\[\] = \{(.*?)\n\};', CONTROLS, re.S)
return {device_class: (on, off) for device_class, on, off in re.findall(r'\{"(\w+)", "([^"]*)", "([^"]*)"\}', block.group(1))}
words = ENGLISH['screen']['ha']['binary']
found = {}
for device_class, on, off in re.findall(r'\{"(\w+)", screen_text::txt::ha_binary_(\w+), screen_text::txt::ha_binary_(\w+)\}', block.group(1)):
found[device_class] = (words[on], words[off])
return found
class BinaryWords(unittest.TestCase):
@@ -41,7 +50,8 @@ class BinaryWords(unittest.TestCase):
for attrs in ({}, {'device_class': 'future_class'}):
self.assertEqual(header_bar.value('binary_sensor.x', {'state': 'on', 'attributes': attrs}), ('On', None))
self.assertEqual(history_card.state_label('binary_sensor', 'off', attrs), 'Off')
self.assertIn('return on ? "On" : "Off";', CONTROLS)
self.assertIn('return screen_text::tr(on ? screen_text::txt::ha_on : screen_text::txt::ha_off);', CONTROLS)
self.assertEqual((ENGLISH['screen']['ha']['on'], ENGLISH['screen']['ha']['off']), ('On', 'Off'))
def test_the_state_message_already_carries_the_class(self):
tile = {'entity': 'binary_sensor.front_door', 'name': ''}
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "../components/smart_display/calibration_math.h"
#include <cassert>
int main() {
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
// clang++ -std=c++17 -Wall -Wextra -Werror -I. tests/test_camera_view.cpp -o /tmp/test_camera_view && /tmp/test_camera_view
#include "../components/smart_display/camera_view.h"
#include <cassert>
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "../components/smart_display/chunk_transport.h"
#include <cassert>
int main() {
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "../components/smart_display/cyd_ui.h"
#include <cassert>
#include <limits>
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
// c++ -std=c++17 -Wall -Wextra -pedantic tests/test_effects_page.cpp -o /tmp/test_effects_page && /tmp/test_effects_page
// The model of a light's effects page (firmware 0.2.70+): what it can show, how a number maps to its slider, the texts.
#define EFFECTS_PAGE_TEST
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "components/smart_display/header_bar.h"
#include <cassert>
#include <cstdio>
+9 -5
View File
@@ -18,6 +18,8 @@ import tile_icons # noqa: E402
from core import FIRMWARE_VERSION, HEADER_MIN_FIRMWARE, discover, header_items, validate_header, validate_layout # noqa: E402
FIRMWARE = (ROOT / 'components/smart_display/header_bar.h').read_text()
# The firmware's words live in the translations since app 0.2.90 (screen.time in English, the language of reference).
FIRMWARE += json.dumps(json.loads((ROOT / 'screen_manager/translations/en.json').read_text(encoding='utf-8'))['screen']['time'])
TILES = (ROOT / 'components/smart_display/runtime_tiles.h').read_text()
EDITOR = (ROOT / 'web/src/model/topbar.ts').read_text()
PROFILES = ('guition-4848s040.yaml', 'home-like-2432s028.yaml')
@@ -109,8 +111,10 @@ class TextTests(unittest.TestCase):
self.assertEqual(header_bar.value(eid, value, registry.get(eid), {'temperature': '°C'}, tz), expected, eid)
def test_text_keeps_to_the_font_and_its_length(self):
self.assertEqual(header_bar.clean_text('Café “Sun” 25 m³'), 'Café Sun 25 m³')
self.assertEqual(header_bar.clean_text('Ångström ✓'), 'Angström')
# Every letter European languages write with stays (app 0.2.90); anything else folds to its base letter or goes.
self.assertEqual(header_bar.clean_text('Café “Sun” 25 m³'), 'Café “Sun” 25 m³')
self.assertEqual(header_bar.clean_text('Ångström ✓'), 'Ångström')
self.assertEqual(header_bar.clean_text('Ǻngström'), 'Angström')
self.assertLessEqual(len(header_bar.clean_text('é' * 60).encode()), header_bar.TEXT_BYTES)
def test_active_colour_and_icon(self):
@@ -228,10 +232,10 @@ class ParityTests(unittest.TestCase):
# Firmware 0.2.73+: until the first layout the text and a spinner stand in the middle and the name stays empty;
# the first layout deletes them, so no spinner turns behind the tiles.
render = TILES.split('inline void render(lv_obj_t *room) {', 1)[1].split('\n}', 1)[0]
self.assertIn('if (!model.configured) boot_status(lv_obj_get_parent(room), !ha_connected() ? '
'"Connecting to Home Assistant" : "Waiting for ESP Screens");', render)
self.assertIn('if (!model.configured) boot_status(lv_obj_get_parent(room), tr(!ha_connected() ? '
'txt::status_connecting : txt::status_waiting));', render)
self.assertIn('else if (boot_panel) { lv_obj_delete(boot_panel);', render)
self.assertIn('label(room, !model.configured ? "" :', render)
self.assertIn('label(room, !model.configured ? std::string() :', render)
boot = TILES.split('inline void boot_status(', 1)[1].split('\n}', 1)[0]
self.assertIn('boot_spinner = spinner_create(boot_panel,', boot)
# Under the tiles, the cards and an alert, in the name's place.
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
// clang++ -std=c++17 -Wall -Wextra -Werror -I. tests/test_history_view.cpp -o /tmp/test_history_view && /tmp/test_history_view
#include "../components/smart_display/history_view.h"
#include <cassert>
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#define LIGHT_CONTROLS_TEST
#include "../components/smart_display/light_controls.h"
#include <cassert>
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
// clang++ -std=c++17 -Wall -Wextra -Werror -I. tests/test_media_card.cpp -o /tmp/test_media_card && /tmp/test_media_card
#include "../components/smart_display/media_card.h"
#include <cassert>
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "../components/smart_display/runtime_model.h"
#include <cassert>
int main() {
+12 -9
View File
@@ -47,14 +47,14 @@ ENTITY_IDS = {
'standby_seconds': 'number.office_1_standby_after', 'standby_brightness': 'number.office_1_standby_brightness',
'night_enabled': 'switch.office_1_night_mode', 'night_start': 'time.office_1_night_starts',
'night_end': 'time.office_1_night_ends', 'night_brightness': 'number.office_1_night_brightness',
'clock_24h': 'switch.office_1_24_hour_clock', 'auto_home': 'switch.office_1_back_to_page_1',
'auto_home': 'switch.office_1_back_to_page_1',
'auto_home_seconds': 'number.office_1_back_to_page_1_after', 'home_on_standby': 'switch.office_1_back_to_page_1_on_standby',
'swipe_pages': 'switch.office_1_swipe_between_pages', 'rotation': 'select.office_1_rotation',
'dark_mode': 'switch.office_1_dark_mode', 'page_buttons': 'switch.office_1_page_buttons',
}
STATES = {'brightness': '80.0', 'standby_enabled': 'on', 'standby_seconds': '600.0', 'standby_brightness': '20.0',
'night_enabled': 'on', 'night_start': '22:30:00', 'night_end': '07:00:00', 'night_brightness': '5.0',
'clock_24h': 'on', 'auto_home': 'on', 'auto_home_seconds': '120.0', 'home_on_standby': 'off',
'auto_home': 'on', 'auto_home_seconds': '120.0', 'home_on_standby': 'off',
'swipe_pages': 'on', 'rotation': '90°', 'dark_mode': 'off', 'page_buttons': 'on'}
@@ -109,7 +109,8 @@ def fake_ha(firmware=FIRMWARE_VERSION, owned=True, guition=True):
class CoreSettings(unittest.TestCase):
def test_every_setting_the_editor_offers_has_an_entity(self):
self.assertEqual(set(SETTING_ENTITIES), set(SETTING_RULES) - {'show_clock'})
# 12 or 24 hours is Settings -> Language & region's, for every screen at once (app 0.2.90).
self.assertEqual(set(SETTING_ENTITIES), set(SETTING_RULES) - {'show_clock', 'clock_24h'})
self.assertEqual(SETTING_ENTITIES_MIN_FIRMWARE, '0.2.49')
self.assertLessEqual(tuple(map(int, SETTING_ENTITIES_MIN_FIRMWARE.split('.'))), tuple(map(int, FIRMWARE_VERSION.split('.'))))
@@ -179,7 +180,7 @@ class OwnedSettings(unittest.IsolatedAsyncioTestCase):
self.assertNotIn('settings', m.layouts['text.screen'], 'a screen that owns its settings has none stored')
view = m.settings_view(m.screen('text.screen'))
self.assertEqual((view['owner'], view['unavailable']), ('screen', []))
self.assertEqual(view['keys'], [key for key in SETTING_RULES if key != 'show_clock'])
self.assertEqual(view['keys'], [key for key in SETTING_RULES if key not in ('show_clock', 'clock_24h')])
self.assertEqual({key: view['values'][key] for key in ('brightness', 'night_start', 'rotation', 'home_on_standby')},
{'brightness': 80, 'night_start': 1350, 'rotation': 90, 'home_on_standby': False})
m.ha.states[ENTITY_IDS['night_end']] = {'state': 'unavailable'}
@@ -225,12 +226,11 @@ class OwnedSettings(unittest.IsolatedAsyncioTestCase):
await m.change_settings('text.screen', {'brightness': 10})
self.assertEqual(m.ha.calls, [('number.set_value', {'entity_id': ENTITY_IDS['brightness'], 'value': 10})])
m.ha.calls.clear()
await m.change_settings('text.screen', {'night_start': 1320, 'rotation': 270, 'clock_24h': False})
await m.change_settings('text.screen', {'night_start': 1320, 'rotation': 270})
self.assertEqual(m.ha.calls, [('time.set_value', {'entity_id': ENTITY_IDS['night_start'], 'time': '22:00:00'}),
('switch.turn_off', {'entity_id': ENTITY_IDS['clock_24h']}),
('select.select_option', {'entity_id': ENTITY_IDS['rotation'], 'option': '270°'})])
m.ha.calls.clear()
for bad in ({'standby_brightness': 95}, {'show_clock': False}, {'beep': True}, {}, None, {'brightness': '50'}):
for bad in ({'standby_brightness': 95}, {'show_clock': False}, {'clock_24h': False}, {'beep': True}, {}, None, {'brightness': '50'}):
with self.assertRaises(ValueError):
await m.change_settings('text.screen', bad)
m.ha.states[ENTITY_IDS['night_end']] = {'state': 'unavailable'}
@@ -569,9 +569,12 @@ class Editor(unittest.TestCase):
def test_the_rows_are_the_settings_of_the_screen_page(self):
keys = re.findall(r'\{ key: "(\w+)", label: "[^"]+", kind: "(\w+)"', self.script)
self.assertEqual([key for key, _ in keys], [key for key in SETTING_RULES if key != 'show_clock'][:0] or [key for key, _ in keys])
self.assertEqual({key for key, _ in keys}, set(SETTING_RULES) - {'show_clock'})
# 12 or 24 hours is Settings -> Language & region's, for every screen at once (app 0.2.90).
self.assertEqual({key for key, _ in keys}, set(SETTING_RULES) - {'show_clock', 'clock_24h'})
# Labels and steps as on the screen: the same rows, the same -/+ steps, the same duration ladder.
page_labels = set(re.findall(r'(?:number|toggle|duration|moment|choice)\("([^"]+)"', SCREEN_PAGE))
# The page's labels are keys into the translations since app 0.2.90; English is the reference.
english = json.loads((ROOT / 'screen_manager/translations/en.json').read_text(encoding='utf-8'))['screen']['settings']
page_labels = {english[key] for key in re.findall(r'(?:number|toggle|duration|moment|choice)\(screen_text::txt::settings_(\w+)', SCREEN_PAGE)}
groups = self.script[self.script.index('const SETTING_GROUPS = ['):self.script.index('export type SettingRow')]
script_labels = set(re.findall(r'label: "([^"]+)"', groups))
self.assertEqual(page_labels, script_labels)
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "components/smart_display/screen_settings.h"
#include <cassert>
int main() {
+14 -12
View File
@@ -1,4 +1,5 @@
#define SETTINGS_SCREEN_TEST
#include "screen_text_en.h"
#include "../components/smart_display/settings_screen.h"
#include <cassert>
#include <string>
@@ -8,7 +9,7 @@ using namespace settings_screen;
// The row of a page by its label, so a test says what it means.
static const Row &row_named(const Page &page, const std::string &label) {
for (uint8_t i = 0; i < page.count; ++i)
if (label == page.rows[i].label) return page.rows[i];
if (label == label_text(page.rows[i])) return page.rows[i];
assert(false);
return page.rows[0];
}
@@ -17,7 +18,7 @@ int main() {
// ---- the duration ladder: seconds low down, quarters of an hour up top, and symmetric ----
assert(ladder_step(60) == 30 && ladder_step(600) == 60 && ladder_step(1800) == 300);
assert(ladder_step(3600) == 900 && ladder_step(36000) == 1800);
Row timeout = duration("After", nullptr, nullptr, 30, 3600);
Row timeout = duration(screen_text::txt::settings_after, nullptr, nullptr, 30, 3600);
assert(stepped(timeout, 120, 1) == 150 && stepped(timeout, 120, -1) == 90);
assert(stepped(timeout, 300, 1) == 360 && stepped(timeout, 360, -1) == 300);
assert(stepped(timeout, 300, -1) == 270 && stepped(timeout, 270, 1) == 300);
@@ -25,9 +26,9 @@ int main() {
assert(at_end(timeout, 30, -1) && at_end(timeout, 3600, 1) && !at_end(timeout, 120, -1));
// ---- a number stops at its ends, a moment walks around midnight ----
Row percent = number("Brightness", nullptr, nullptr, 5, 100, 5, "%");
Row percent = number(screen_text::txt::settings_brightness, nullptr, nullptr, 5, 100, 5, "%");
assert(stepped(percent, 100, 1) == 100 && stepped(percent, 5, -1) == 5 && stepped(percent, 95, 1) == 100);
Row start = moment("Starts", nullptr, nullptr);
Row start = moment(screen_text::txt::settings_starts, nullptr, nullptr);
assert(stepped(start, 1320, 1) == 1335 && stepped(start, 1425, 1) == 0 && stepped(start, 0, -1) == 1425);
assert(!at_end(start, 0, -1));
// Holding the key walks whole hours, and first lands on the hour it is in.
@@ -64,7 +65,7 @@ int main() {
assert(screen_settings::current.standby_brightness == 30); // never above the normal brightness
assert(screen_settings::current.valid());
// Dark mode (firmware 0.2.54+) sits right under Brightness, a switch of its own outside the frozen block.
assert(light.count == 5 && light.rows[1].kind == Kind::toggle && std::string(light.rows[1].label) == "Dark mode");
assert(light.count == 5 && light.rows[1].kind == Kind::toggle && std::string(label_text(light.rows[1])) == "Dark mode");
const Row &dark = row_named(light, "Dark mode");
assert(value_text(dark) == "Off");
dark.write(1);
@@ -84,10 +85,11 @@ int main() {
buttons.write(1);
assert(page_buttons == 1);
}
const Row &clock = row_named(screen, "Clock");
assert(clock.option_count == 2 && value_text(clock) == "24 hour");
clock.write(0);
assert(screen_settings::current.clock_24h == 0 && value_text(clock) == "12 hour");
// 12 or 24 hours is chosen for every screen at once in ESP Screens (app 0.2.90): the page has no row for it, and
// the value still arrives through set() and the layout message.
for (uint8_t i = 0; i < screen.count; ++i) assert(std::string(label_text(screen.rows[i])) != "Clock");
set("clock_24h", 0);
assert(screen_settings::current.clock_24h == 0);
const Row &home = row_named(screen, "Back to page 1");
home.write(0);
assert(auto_home == 0 && value_text(home) == "Off");
@@ -107,17 +109,17 @@ int main() {
assert(PAGE_COUNT == 5);
for (uint8_t p = 0; p < PAGE_COUNT; ++p) {
const Page &page = pages[p];
assert(page.title && page.count && page.count <= 8);
assert(page.title != NO_TEXT && *screen_text::tr(page.title) && page.count && page.count <= 8);
for (uint8_t i = 0; i < page.count; ++i) {
const Row &row = page.rows[i];
assert(row.label && *row.label);
assert(row.label != NO_TEXT && *label_text(row));
if (row.kind == Kind::page) assert(row.opens > 0 && row.opens < PAGE_COUNT && *row.icon);
if (row.kind == Kind::toggle || row.kind == Kind::choice || row.kind == Kind::number ||
row.kind == Kind::duration || row.kind == Kind::moment)
assert(row.read && row.write);
if (row.kind == Kind::number) assert(row.step > 0 && row.high > row.low);
if (row.kind == Kind::duration) assert(row.high > row.low);
if (row.kind == Kind::choice) assert(row.options && row.option_count);
if (row.kind == Kind::choice) assert((row.options || row.option_keys != NO_TEXT) && row.option_count);
if (row.kind == Kind::info) assert(row.text);
if (row.kind == Kind::action) assert(row.run && *row.icon);
}
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
// The colour table (firmware 0.2.54): the light look stays the design it was, the dark look reads well beside a bed.
// clang++ -std=c++17 -Wall -Wextra -Werror -I. tests/test_theme.cpp -o /tmp/test_theme && /tmp/test_theme
#define THEME_TEST
+1 -1
View File
@@ -151,7 +151,7 @@ class Setting(unittest.TestCase):
self.assertEqual(DARK_MODE_MIN_FIRMWARE, '0.2.54')
self.assertLessEqual(tuple(map(int, DARK_MODE_MIN_FIRMWARE.split('.'))), tuple(map(int, FIRMWARE_VERSION.split('.'))))
page = (COMPONENT / 'settings_screen.h').read_text()
self.assertIn('toggle("Dark mode", []() -> int32_t { return dark_mode; },', page)
self.assertIn('toggle(screen_text::txt::settings_dark_mode, []() -> int32_t { return dark_mode; },', page)
self.assertIn('else if (key == "dark_mode") reported = dark_mode = flag(value);', page)
editor = (ROOT / 'web/src/store.ts').read_text()
self.assertIn('{ key: "dark_mode", label: "Dark mode", kind: "toggle" },', editor)
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
// c++ -std=c++17 -Wall -Wextra -pedantic tests/test_tile_controls.cpp -o /tmp/test_tile_controls && /tmp/test_tile_controls
#define THEME_TEST
#include "../components/smart_display/tile_controls.h"
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#include "components/smart_display/tile_icon.h"
#include <cassert>
int main() {
+1
View File
@@ -1,3 +1,4 @@
#include "screen_text_en.h"
#define THEME_TEST
#include "components/smart_display/tile_palette.h"
#include <cassert>