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
+89 -51
View File
@@ -18,6 +18,9 @@ substitutions:
DASHBOARD_INBOX_INTERNAL: "false"
CALIBRATION_ON_BOOT: "false" # a bench build: start on the touch test page
ROOM_NAME: "" # top left until ESP Screens sends the screen name
# The language of every text on the screen (app 0.2.90): a file in screen_manager/translations, English for any
# text it lacks. ESP Screens writes it into the screen's own YAML, from Home Assistant's language or its Settings.
LANGUAGE: "en"
## DIM settings
AUTO_DIM: "true" # true = enable auto dimming
@@ -213,7 +216,7 @@ esphome:
settings_screen::apply = []() { id(apply_screen_settings).execute(); };
settings_screen::report = [](const char *key, int32_t value) { runtime_tiles::setting_event(key, value); };
settings_screen::name_text = []() { return std::string("${DEVICE_FRIENDLY_NAME}"); };
settings_screen::address_text = []() { return id(wifi_ip).state.empty() ? std::string("No network") : id(wifi_ip).state; };
settings_screen::address_text = []() { return id(wifi_ip).state.empty() ? std::string(screen_text::tr(screen_text::txt::status_no_network)) : id(wifi_ip).state; };
settings_screen::firmware_text = []() { return std::string("${SCREEN_FIRMWARE_VERSION}"); };
settings_screen::link_text = []() { return std::string(esphome::api_is_connected() ? "Connected" : "Not connected"); };
settings_screen::restart_device = []() { ESP_LOGI("settings", "restart from the screen"); App.safe_reboot(); };
@@ -663,21 +666,35 @@ font:
id: headline
size: ${FONT_HEADLINE_SIZE}
bpp: 4
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','·']
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5',
'6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
'U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q',
'r','s','t','u','v','w','x','y','z','/','·',
'À','Á','Â','Ã','Ä','Å','Æ','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö','Ø',
'Ù','Ú','Û','Ü','Ý','Þ','ß','à','á','â','ã','ä','å','æ','ç','è','é','ê','ë','ì','í','î','ï','ð',
'ñ','ò','ó','ô','õ','ö','ø','ù','ú','û','ü','ý','þ','ÿ','Ą','ą','Ć','ć','Ę','ę','Ł','ł','Ń','ń',
'Ś','ś','Ź','ź','Ż','ż','Č','č','Ď','ď','Ě','ě','Ň','ň','Ř','ř','Š','š','Ť','ť','Ů','ů','Ž','ž',
'Ĺ','ĺ','Ľ','ľ','Ŕ','ŕ','Ő','ő','Ű','ű','Ă','ă','Ș','ș','Ț','ț','Đ','đ','Ā','ā','Ē','ē','Ģ','ģ',
'Ī','ī','Ķ','ķ','Ļ','ļ','Ņ','ņ','Ū','ū','Ė','ė','Į','į','Ų','ų','Ğ','ğ','İ','ı','Ş','ş','Ċ','ċ',
'Ġ','ġ','Ħ','ħ','Ŵ','ŵ','Ŷ','ŷ','Ŀ','ŀ','Œ','œ','Ÿ','„','“','”','‘','’','«','»','‹','›','¿','¡',
'€','£']
- file: "${FONT_DIR}/Roboto-500.ttf"
id: watch_value
size: ${FONT_WATCH_VALUE_SIZE}
bpp: 4
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','·']
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5',
'6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
'U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q',
'r','s','t','u','v','w','x','y','z','/','·',
'À','Á','Â','Ã','Ä','Å','Æ','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö','Ø',
'Ù','Ú','Û','Ü','Ý','Þ','ß','à','á','â','ã','ä','å','æ','ç','è','é','ê','ë','ì','í','î','ï','ð',
'ñ','ò','ó','ô','õ','ö','ø','ù','ú','û','ü','ý','þ','ÿ','Ą','ą','Ć','ć','Ę','ę','Ł','ł','Ń','ń',
'Ś','ś','Ź','ź','Ż','ż','Č','č','Ď','ď','Ě','ě','Ň','ň','Ř','ř','Š','š','Ť','ť','Ů','ů','Ž','ž',
'Ĺ','ĺ','Ľ','ľ','Ŕ','ŕ','Ő','ő','Ű','ű','Ă','ă','Ș','ș','Ț','ț','Đ','đ','Ā','ā','Ē','ē','Ģ','ģ',
'Ī','ī','Ķ','ķ','Ļ','ļ','Ņ','ņ','Ū','ū','Ė','ė','Į','į','Ų','ų','Ğ','ğ','İ','ı','Ş','ş','Ċ','ċ',
'Ġ','ġ','Ħ','ħ','Ŵ','ŵ','Ŷ','ŷ','Ŀ','ŀ','Œ','œ','Ÿ','„','“','”','‘','’','«','»','‹','›','¿','¡',
'€','£']
# Clock card digits only; the date line reuses the tile title font.
- file: "${FONT_DIR}/Roboto-500.ttf"
@@ -697,31 +714,52 @@ font:
id: label
size: ${FONT_LABEL_SIZE}
bpp: 4
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','·']
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5',
'6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
'U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q',
'r','s','t','u','v','w','x','y','z','/','·',
'À','Á','Â','Ã','Ä','Å','Æ','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö','Ø',
'Ù','Ú','Û','Ü','Ý','Þ','ß','à','á','â','ã','ä','å','æ','ç','è','é','ê','ë','ì','í','î','ï','ð',
'ñ','ò','ó','ô','õ','ö','ø','ù','ú','û','ü','ý','þ','ÿ','Ą','ą','Ć','ć','Ę','ę','Ł','ł','Ń','ń',
'Ś','ś','Ź','ź','Ż','ż','Č','č','Ď','ď','Ě','ě','Ň','ň','Ř','ř','Š','š','Ť','ť','Ů','ů','Ž','ž',
'Ĺ','ĺ','Ľ','ľ','Ŕ','ŕ','Ő','ő','Ű','ű','Ă','ă','Ș','ș','Ț','ț','Đ','đ','Ā','ā','Ē','ē','Ģ','ģ',
'Ī','ī','Ķ','ķ','Ļ','ļ','Ņ','ņ','Ū','ū','Ė','ė','Į','į','Ų','ų','Ğ','ğ','İ','ı','Ş','ş','Ċ','ċ',
'Ġ','ġ','Ħ','ħ','Ŵ','ŵ','Ŷ','ŷ','Ŀ','ŀ','Œ','œ','Ÿ','„','“','”','‘','’','«','»','‹','›','¿','¡',
'€','£']
- file: "${FONT_DIR}/Roboto-400.ttf"
id: sublabel
size: ${FONT_SUBLABEL_SIZE}
bpp: 4
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','…','·']
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5',
'6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
'U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q',
'r','s','t','u','v','w','x','y','z','/','…','·',
'À','Á','Â','Ã','Ä','Å','Æ','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö','Ø',
'Ù','Ú','Û','Ü','Ý','Þ','ß','à','á','â','ã','ä','å','æ','ç','è','é','ê','ë','ì','í','î','ï','ð',
'ñ','ò','ó','ô','õ','ö','ø','ù','ú','û','ü','ý','þ','ÿ','Ą','ą','Ć','ć','Ę','ę','Ł','ł','Ń','ń',
'Ś','ś','Ź','ź','Ż','ż','Č','č','Ď','ď','Ě','ě','Ň','ň','Ř','ř','Š','š','Ť','ť','Ů','ů','Ž','ž',
'Ĺ','ĺ','Ľ','ľ','Ŕ','ŕ','Ő','ő','Ű','ű','Ă','ă','Ș','ș','Ț','ț','Đ','đ','Ā','ā','Ē','ē','Ģ','ģ',
'Ī','ī','Ķ','ķ','Ļ','ļ','Ņ','ņ','Ū','ū','Ė','ė','Į','į','Ų','ų','Ğ','ğ','İ','ı','Ş','ş','Ċ','ċ',
'Ġ','ġ','Ħ','ħ','Ŵ','ŵ','Ŷ','ŷ','Ŀ','ŀ','Œ','œ','Ÿ','„','“','”','‘','’','«','»','‹','›','¿','¡',
'€','£']
- file: "${FONT_DIR}/Roboto-400.ttf"
id: sublabel_big
size: ${FONT_SUBLABEL_BIG_SIZE}
bpp: 4
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','…','·',"'",'#','*','=',';','²','³','µ','é','è','ë','ê','ï','î','ó','ô','à','á','â','ç','ñ','ú','û','–']
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5',
'6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
'U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q',
'r','s','t','u','v','w','x','y','z','/','…','·',"'",'#','*','=',';','²','³','µ','–',
'À','Á','Â','Ã','Ä','Å','Æ','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö','Ø',
'Ù','Ú','Û','Ü','Ý','Þ','ß','à','á','â','ã','ä','å','æ','ç','è','é','ê','ë','ì','í','î','ï','ð',
'ñ','ò','ó','ô','õ','ö','ø','ù','ú','û','ü','ý','þ','ÿ','Ą','ą','Ć','ć','Ę','ę','Ł','ł','Ń','ń',
'Ś','ś','Ź','ź','Ż','ż','Č','č','Ď','ď','Ě','ě','Ň','ň','Ř','ř','Š','š','Ť','ť','Ů','ů','Ž','ž',
'Ĺ','ĺ','Ľ','ľ','Ŕ','ŕ','Ő','ő','Ű','ű','Ă','ă','Ș','ș','Ț','ț','Đ','đ','Ā','ā','Ē','ē','Ģ','ģ',
'Ī','ī','Ķ','ķ','Ļ','ļ','Ņ','ņ','Ū','ū','Ė','ė','Į','į','Ų','ų','Ğ','ğ','İ','ı','Ş','ş','Ċ','ċ',
'Ġ','ġ','Ħ','ħ','Ŵ','ŵ','Ŷ','ŷ','Ŀ','ŀ','Œ','œ','Ÿ','„','“','”','‘','’','«','»','‹','›','¿','¡',
'€','£']
# Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0)
# Source: https://github.com/Templarian/MaterialDesign-Webfont
@@ -1157,6 +1195,12 @@ text_sensor:
entity_category: diagnostic
lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};'
update_interval: 60s
# The language this firmware was built in (0.2.90+): ESP Screens offers an update when it differs from the chosen one.
- platform: template
name: "Screen language"
entity_category: diagnostic
lambda: 'return {screen_text::LANGUAGE};'
update_interval: 60s
# ESP Screen Manager matches the screen to its YAML profile and OTA address.
- platform: template
name: "Device name"
@@ -1216,17 +1260,6 @@ switch:
- lambda: 'settings_screen::set("dark_mode", 1);'
turn_off_action:
- lambda: 'settings_screen::set("dark_mode", 0);'
- platform: template
id: setting_clock_24h
name: "24-hour clock"
icon: "mdi:clock-outline"
entity_category: config
restore_mode: DISABLED
lambda: return screen_settings::current.clock_24h;
turn_on_action:
- lambda: 'settings_screen::set("clock_24h", 1);'
turn_off_action:
- lambda: 'settings_screen::set("clock_24h", 0);'
- platform: template
id: setting_auto_home
name: "Back to page 1"
@@ -1461,7 +1494,7 @@ lvgl:
width: ${ALERT_TEXT_W}
height: ${ALERT_TITLE_H}
long_mode: DOT
text: "Notification"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::alert_notification));'
text_font: headline
clickable: false
- label:
@@ -1505,7 +1538,7 @@ lvgl:
- label:
id: alert_ok_label
align: CENTER
text: "OK"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::alert_ok));'
text_font: headline
clickable: false
@@ -2717,7 +2750,7 @@ lvgl:
id: climate_set_label
align: CENTER
y: ${CLIMATE_CAPTION_Y}
text: "Target"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_target));'
text_font: sublabel
styles: paint_subtle
clickable: false
@@ -2912,7 +2945,7 @@ lvgl:
- label:
align: TOP_MID
y: ${PICKER_TITLE_Y}
text: "Mode"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_mode));'
text_font: sublabel_big
clickable: false
@@ -2954,7 +2987,7 @@ lvgl:
widgets:
- label:
align: CENTER
text: "Off"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_off));'
text_font: sublabel_big
clickable: false
- obj:
@@ -2973,7 +3006,7 @@ lvgl:
widgets:
- label:
align: CENTER
text: "Heat"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_heat));'
text_font: sublabel_big
clickable: false
- obj:
@@ -2992,7 +3025,7 @@ lvgl:
widgets:
- label:
align: CENTER
text: "Cool"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_cool));'
text_font: sublabel_big
clickable: false
- obj:
@@ -3011,7 +3044,7 @@ lvgl:
widgets:
- label:
align: CENTER
text: "Heat/Cool"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_heat_cool));'
text_font: sublabel_big
clickable: false
- obj:
@@ -3030,7 +3063,7 @@ lvgl:
widgets:
- label:
align: CENTER
text: "Auto"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_auto));'
text_font: sublabel_big
clickable: false
- obj:
@@ -3049,7 +3082,7 @@ lvgl:
widgets:
- label:
align: CENTER
text: "Dry"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_dry));'
text_font: sublabel_big
clickable: false
- obj:
@@ -3068,7 +3101,7 @@ lvgl:
widgets:
- label:
align: CENTER
text: "Fan only"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_fan_only));'
text_font: sublabel_big
clickable: false
@@ -3076,7 +3109,7 @@ lvgl:
id: cfan_title
width: 100%
height: ${PICKER_TITLE_H}
text: "Fan"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_fan));'
text_font: sublabel
styles: paint_subtle
text_align: CENTER
@@ -3165,7 +3198,7 @@ lvgl:
id: cswing_title
width: 100%
height: ${PICKER_TITLE_H}
text: "Swing"
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_swing));'
text_font: sublabel
styles: paint_subtle
text_align: CENTER
@@ -3310,7 +3343,7 @@ lvgl:
- label:
align: TOP_MID
y: ${TOUCH_TEST_TITLE_Y}
text: ${TOUCH_TEST_TITLE}
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::${TOUCH_TEST_TITLE}));'
text_font: ${TOUCH_TEST_TITLE_FONT}
- obj:
x: ${TOUCH_TEST_INSET}
@@ -4192,6 +4225,11 @@ script:
}
# The screen's texts in the LANGUAGE substitution's language: the build puts that one language in the firmware
# (components/smart_display).
smart_display:
language: ${LANGUAGE}
# HA blueprint transport; never publishes the JSON command as an HA state.
text:
- platform: smart_display