Weekdays that fit the weather columns, the top bar's own abbreviation, tile texts that fit, and a check that measures them
- The CYD's weather columns hold two letters beside the icon: every language has its own two-letter days (the CLDR's short width, Polish nd pn wt); French mar became nar there. check fails on a longer one. - The top bar takes the language's abbreviation (sam. 19 sept., Sa., 19. Sept.), English keeps Sa 19 Sep. - The editor's clock and date follow the screens: 7:05 PM in the top bar, 7:05 on the clock card. - A long weather word ends in an ellipsis before the days. - Tile lines that were cut on a screen are shorter (Zum Öffnen, Ouvrir, Otwórz, Sin imagen, ...); check measures them with the screens' font against the tile width of both boards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -88,11 +88,13 @@ inline std::string ago_text(int64_t then, int64_t now) {
|
|||||||
return plural(txt::time_years_ago, n(31536000));
|
return plural(txt::time_years_ago, n(31536000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Mo 14 Sep" in English, "ma 14 sep" in Dutch (screen.date.top_bar); day_of_week 1 is Sunday, as ESPHome counts.
|
// "Mo 14 Sep" in English, "za 19 sep" in Dutch, "sam. 19 sept." in French (screen.date.top_bar, with the language's
|
||||||
|
// abbreviation as {weekday} or its two letters as {weekday_min}); day_of_week 1 is Sunday, as ESPHome counts.
|
||||||
inline std::string date_text(int day_of_week, int day_of_month, int month) {
|
inline std::string date_text(int day_of_week, int day_of_month, int month) {
|
||||||
using namespace screen_text;
|
using namespace screen_text;
|
||||||
if (day_of_week < 1 || day_of_week > 7 || month < 1 || month > 12) return "—";
|
if (day_of_week < 1 || day_of_week > 7 || month < 1 || month > 12) return "—";
|
||||||
std::string text = fill(txt::date_top_bar, "weekday", tr(txt::date_weekdays_min + day_of_week - 1));
|
std::string text = fill(txt::date_top_bar, "weekday", tr(txt::date_weekdays_short + day_of_week - 1));
|
||||||
|
text = fill(text, "weekday_min", tr(txt::date_weekdays_min + day_of_week - 1));
|
||||||
text = fill(text, "day", std::to_string(day_of_month));
|
text = fill(text, "day", std::to_string(day_of_month));
|
||||||
return fill(text, "month", tr(txt::date_months_short + month - 1));
|
return fill(text, "month", tr(txt::date_months_short + month - 1));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2617,7 +2617,9 @@ inline void render_forecast(Widgets &w,const Tile &t,bool large,int width,int he
|
|||||||
auto *icon=part_label(w,0,w.icon_font,0,y+(std::max(icon_h,temp_h)-icon_h)/2,icon_h+4,LV_TEXT_ALIGN_LEFT,t.available()?weather_icon(t.state):"\U000F0595");
|
auto *icon=part_label(w,0,w.icon_font,0,y+(std::max(icon_h,temp_h)-icon_h)/2,icon_h+4,LV_TEXT_ALIGN_LEFT,t.available()?weather_icon(t.state):"\U000F0595");
|
||||||
lv_obj_set_width(icon,lv_font_get_line_height(w.icon_font)+4);
|
lv_obj_set_width(icon,lv_font_get_line_height(w.icon_font)+4);
|
||||||
part_label(w,1,temp_font,icon_h+6,y+(std::max(icon_h,temp_h)-temp_h)/2,left-icon_h-6,LV_TEXT_ALIGN_LEFT,std::isfinite(t.current)?b:"");
|
part_label(w,1,temp_font,icon_h+6,y+(std::max(icon_h,temp_h)-temp_h)/2,left-icon_h-6,LV_TEXT_ALIGN_LEFT,std::isfinite(t.current)?b:"");
|
||||||
part_label(w,2,w.value_font,0,y+std::max(icon_h,temp_h)+2,left-4,LV_TEXT_ALIGN_LEFT,weather_text(t.state));
|
// Home Assistant's word for the weather can be long ("częściowe zachmurzenie"): it ends in an ellipsis before the days.
|
||||||
|
auto *condition=part_label(w,2,w.value_font,0,y+std::max(icon_h,temp_h)+2,left-4,LV_TEXT_ALIGN_LEFT,weather_text(t.state));
|
||||||
|
if(lv_label_get_long_mode(condition)!=LV_LABEL_LONG_DOT)lv_label_set_long_mode(condition,LV_LABEL_LONG_DOT);
|
||||||
int column=(width-left)/5;
|
int column=(width-left)/5;
|
||||||
for(unsigned k=0;k<5;++k){
|
for(unsigned k=0;k<5;++k){
|
||||||
static const Forecast no_day;int x=left+k*column;bool has=k<t.extra().forecast.size();const auto &f=has?t.extra().forecast[k]:no_day;
|
static const Forecast no_day;int x=left+k*column;bool has=k<t.extra().forecast.size();const auto &f=has?t.extra().forecast[k]:no_day;
|
||||||
|
|||||||
+5
-2
@@ -27,7 +27,9 @@ Three parts fill themselves. Don't change them by hand:
|
|||||||
[Home Assistant's translations](https://developers.home-assistant.io/docs/translations/); we pick it up from there.
|
[Home Assistant's translations](https://developers.home-assistant.io/docs/translations/); we pick it up from there.
|
||||||
- `screen.date` holds day and month names and the order of a date. `screen.number` says how numbers are written.
|
- `screen.date` holds day and month names and the order of a date. `screen.number` says how numbers are written.
|
||||||
`screen.time.am` and `.pm` are the day periods. All of this comes from the Unicode CLDR, the same data every browser
|
`screen.time.am` and `.pm` are the day periods. All of this comes from the Unicode CLDR, the same data every browser
|
||||||
uses, through `tools/i18n.py cldr`. `screen.number.percent` follows Home Assistant's own rule: a space before `%` in
|
uses, through `tools/i18n.py cldr`. One list is yours: `screen.date.weekdays_min`, the days of the weather card in
|
||||||
|
two letters (`di`, `lu`, `ma` in French), because the small screen has room for no more. `cldr` starts a new language
|
||||||
|
with a guess and then leaves the list alone. `screen.number.percent` follows Home Assistant's own rule: a space before `%` in
|
||||||
Czech, German, Finnish, French, Slovak and Swedish, none in the other languages.
|
Czech, German, Finnish, French, Slovak and Swedish, none in the other languages.
|
||||||
- `_meta.clock`, 12 or 24 hours, also comes from the CLDR.
|
- `_meta.clock`, 12 or 24 hours, also comes from the CLDR.
|
||||||
|
|
||||||
@@ -44,7 +46,8 @@ there is a `sv.json`, but already the Swedish 24-hour clock and `1 234,5`.
|
|||||||
- wrong words;
|
- wrong words;
|
||||||
- words that differ from the ones the Home Assistant app uses in your language (the Home Assistant app is the
|
- words that differ from the ones the Home Assistant app uses in your language (the Home Assistant app is the
|
||||||
reference: *Dashboard*, *Automation*, *Entity* are what people know);
|
reference: *Dashboard*, *Automation*, *Entity* are what people know);
|
||||||
- texts that are too long for a small screen.
|
- texts that are too long for a small screen. `tools/i18n.py check` measures the line under a tile's name with the
|
||||||
|
screens' own font and says when it doesn't fit; for the rest, compare with the English.
|
||||||
3. Look at it for real, if you can. In ESP Screens open **Settings → Language & region** and choose your language. The
|
3. Look at it for real, if you can. In ESP Screens open **Settings → Language & region** and choose your language. The
|
||||||
editor itself follows the language of your Home Assistant profile. The screens show the new texts after their
|
editor itself follows the language of your Home Assistant profile. The screens show the new texts after their
|
||||||
firmware update.
|
firmware update.
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+8
-8
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
<title>ESP Screens</title>
|
<title>ESP Screens</title>
|
||||||
<script type="module" crossorigin src="./assets/index-CRR4xeFy.js"></script>
|
<script type="module" crossorigin src="./assets/index-Cw-mwwtp.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-B7AxjXTR.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-B7AxjXTR.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
"hours_minutes": "{h} Std. {m}"
|
"hours_minutes": "{h} Std. {m}"
|
||||||
},
|
},
|
||||||
"tile": {
|
"tile": {
|
||||||
"tap_to_open": "Zum Öffnen tippen",
|
"tap_to_open": "Zum Öffnen",
|
||||||
"page": "Seite {n}",
|
"page": "Seite {n}",
|
||||||
"refused": "Abgelehnt",
|
"refused": "Abgelehnt",
|
||||||
"command_sent": "Befehl gesendet...",
|
"command_sent": "Befehl gesendet...",
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"muted": "Stumm",
|
"muted": "Stumm",
|
||||||
"not_playing": "Keine Wiedergabe",
|
"not_playing": "Nichts läuft",
|
||||||
"loading": "Wird geladen"
|
"loading": "Wird geladen"
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
"loading": "Bild wird geladen",
|
"loading": "Bild wird geladen",
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
"recording": "Aufnahme",
|
"recording": "Aufnahme",
|
||||||
"tap_to_view": "Zum Ansehen tippen",
|
"tap_to_view": "Ansehen",
|
||||||
"no_image_yet": "Noch kein Bild"
|
"no_image_yet": "Noch kein Bild"
|
||||||
},
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
"Nov",
|
"Nov",
|
||||||
"Dec"
|
"Dec"
|
||||||
],
|
],
|
||||||
"top_bar": "{weekday} {day} {month}",
|
"top_bar": "{weekday_min} {day} {month}",
|
||||||
"full": "{weekday} {day} {month}",
|
"full": "{weekday} {day} {month}",
|
||||||
"day_month": "{day} {month}",
|
"day_month": "{day} {month}",
|
||||||
"weekday_time": "{weekday} {time}"
|
"weekday_time": "{weekday} {time}"
|
||||||
|
|||||||
@@ -50,13 +50,13 @@
|
|||||||
"sáb"
|
"sáb"
|
||||||
],
|
],
|
||||||
"weekdays_min": [
|
"weekdays_min": [
|
||||||
"dom",
|
"do",
|
||||||
"lun",
|
"lu",
|
||||||
"mar",
|
"ma",
|
||||||
"mié",
|
"mi",
|
||||||
"jue",
|
"ju",
|
||||||
"vie",
|
"vi",
|
||||||
"sáb"
|
"sá"
|
||||||
],
|
],
|
||||||
"months": [
|
"months": [
|
||||||
"enero",
|
"enero",
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"muted": "Silenciado",
|
"muted": "Silenciado",
|
||||||
"not_playing": "No se reproduce nada",
|
"not_playing": "Inactivo",
|
||||||
"loading": "Cargando"
|
"loading": "Cargando"
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
"live": "En directo",
|
"live": "En directo",
|
||||||
"recording": "Grabando",
|
"recording": "Grabando",
|
||||||
"tap_to_view": "Toca para ver",
|
"tap_to_view": "Toca para ver",
|
||||||
"no_image_yet": "Aún no hay imagen"
|
"no_image_yet": "Sin imagen"
|
||||||
},
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"title": "Sigue la calibración",
|
"title": "Sigue la calibración",
|
||||||
|
|||||||
@@ -50,13 +50,13 @@
|
|||||||
"sam."
|
"sam."
|
||||||
],
|
],
|
||||||
"weekdays_min": [
|
"weekdays_min": [
|
||||||
"dim",
|
"di",
|
||||||
"lun",
|
"lu",
|
||||||
"mar",
|
"ma",
|
||||||
"mer",
|
"me",
|
||||||
"jeu",
|
"je",
|
||||||
"ven",
|
"ve",
|
||||||
"sam"
|
"sa"
|
||||||
],
|
],
|
||||||
"months": [
|
"months": [
|
||||||
"janvier",
|
"janvier",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"hours_minutes": "{h} h {m}"
|
"hours_minutes": "{h} h {m}"
|
||||||
},
|
},
|
||||||
"tile": {
|
"tile": {
|
||||||
"tap_to_open": "Touchez pour ouvrir",
|
"tap_to_open": "Ouvrir",
|
||||||
"page": "Page {n}",
|
"page": "Page {n}",
|
||||||
"refused": "Refusé",
|
"refused": "Refusé",
|
||||||
"command_sent": "Commande envoyée...",
|
"command_sent": "Commande envoyée...",
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
"live": "En direct",
|
"live": "En direct",
|
||||||
"recording": "Enregistrement",
|
"recording": "Enregistrement",
|
||||||
"tap_to_view": "Touchez pour voir",
|
"tap_to_view": "Touchez pour voir",
|
||||||
"no_image_yet": "Pas encore d'image"
|
"no_image_yet": "Pas d'image"
|
||||||
},
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"title": "Suivez l'étalonnage",
|
"title": "Suivez l'étalonnage",
|
||||||
|
|||||||
@@ -50,13 +50,13 @@
|
|||||||
"sab"
|
"sab"
|
||||||
],
|
],
|
||||||
"weekdays_min": [
|
"weekdays_min": [
|
||||||
"dom",
|
"do",
|
||||||
"lun",
|
"lu",
|
||||||
"mar",
|
"ma",
|
||||||
"mer",
|
"me",
|
||||||
"gio",
|
"gi",
|
||||||
"ven",
|
"ve",
|
||||||
"sab"
|
"sa"
|
||||||
],
|
],
|
||||||
"months": [
|
"months": [
|
||||||
"gennaio",
|
"gennaio",
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"muted": "Muto",
|
"muted": "Muto",
|
||||||
"not_playing": "Nessuna riproduzione",
|
"not_playing": "Inattivo",
|
||||||
"loading": "Caricamento"
|
"loading": "Caricamento"
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
"live": "Dal vivo",
|
"live": "Dal vivo",
|
||||||
"recording": "Registrazione",
|
"recording": "Registrazione",
|
||||||
"tap_to_view": "Tocca per vedere",
|
"tap_to_view": "Tocca per vedere",
|
||||||
"no_image_yet": "Ancora nessuna immagine"
|
"no_image_yet": "Niente immagine"
|
||||||
},
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"title": "Segui la calibrazione",
|
"title": "Segui la calibrazione",
|
||||||
|
|||||||
@@ -245,7 +245,7 @@
|
|||||||
"loading": "Beeld laden",
|
"loading": "Beeld laden",
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
"recording": "Neemt op",
|
"recording": "Neemt op",
|
||||||
"tap_to_view": "Tik om te bekijken",
|
"tap_to_view": "Tik om te kijken",
|
||||||
"no_image_yet": "Nog geen beeld"
|
"no_image_yet": "Nog geen beeld"
|
||||||
},
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
|
|||||||
@@ -50,13 +50,13 @@
|
|||||||
"sob."
|
"sob."
|
||||||
],
|
],
|
||||||
"weekdays_min": [
|
"weekdays_min": [
|
||||||
"nie",
|
"nd",
|
||||||
"pon",
|
"pn",
|
||||||
"wt",
|
"wt",
|
||||||
"śr",
|
"śr",
|
||||||
"czw",
|
"cz",
|
||||||
"pt",
|
"pt",
|
||||||
"sob"
|
"sb"
|
||||||
],
|
],
|
||||||
"months": [
|
"months": [
|
||||||
"stycznia",
|
"stycznia",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"hours_minutes": "{h} godz. {m}"
|
"hours_minutes": "{h} godz. {m}"
|
||||||
},
|
},
|
||||||
"tile": {
|
"tile": {
|
||||||
"tap_to_open": "Dotknij, aby otworzyć",
|
"tap_to_open": "Otwórz",
|
||||||
"page": "Strona {n}",
|
"page": "Strona {n}",
|
||||||
"refused": "Odrzucono",
|
"refused": "Odrzucono",
|
||||||
"command_sent": "Wysłano polecenie...",
|
"command_sent": "Wysłano polecenie...",
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
},
|
},
|
||||||
"script": {
|
"script": {
|
||||||
"running": "Działa...",
|
"running": "Działa...",
|
||||||
"never_run": "Nigdy nie uruchomiono",
|
"never_run": "Nie uruchamiano",
|
||||||
"last_time": "Ostatnio {time}",
|
"last_time": "Ostatnio {time}",
|
||||||
"yesterday_time": "Wczoraj {time}",
|
"yesterday_time": "Wczoraj {time}",
|
||||||
"last_date": "Ostatnio {date}"
|
"last_date": "Ostatnio {date}"
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
"loading": "Ładowanie obrazu",
|
"loading": "Ładowanie obrazu",
|
||||||
"live": "Na żywo",
|
"live": "Na żywo",
|
||||||
"recording": "Nagrywanie",
|
"recording": "Nagrywanie",
|
||||||
"tap_to_view": "Dotknij, aby zobaczyć",
|
"tap_to_view": "Zobacz",
|
||||||
"no_image_yet": "Brak obrazu"
|
"no_image_yet": "Brak obrazu"
|
||||||
},
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
|
|||||||
@@ -50,13 +50,13 @@
|
|||||||
"sábado"
|
"sábado"
|
||||||
],
|
],
|
||||||
"weekdays_min": [
|
"weekdays_min": [
|
||||||
"dom",
|
"do",
|
||||||
"seg",
|
"sg",
|
||||||
"ter",
|
"te",
|
||||||
"qua",
|
"qu",
|
||||||
"qui",
|
"qi",
|
||||||
"sex",
|
"sx",
|
||||||
"sáb"
|
"sb"
|
||||||
],
|
],
|
||||||
"months": [
|
"months": [
|
||||||
"janeiro",
|
"janeiro",
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"muted": "Sem som",
|
"muted": "Sem som",
|
||||||
"not_playing": "Nada a reproduzir",
|
"not_playing": "Inativo",
|
||||||
"loading": "A carregar"
|
"loading": "A carregar"
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
"live": "Em direto",
|
"live": "Em direto",
|
||||||
"recording": "A gravar",
|
"recording": "A gravar",
|
||||||
"tap_to_view": "Toque para ver",
|
"tap_to_view": "Toque para ver",
|
||||||
"no_image_yet": "Ainda sem imagem"
|
"no_image_yet": "Sem imagem"
|
||||||
},
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"title": "Siga a calibração",
|
"title": "Siga a calibração",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const char *const screen_text::TABLE[] = {
|
|||||||
"Oct",
|
"Oct",
|
||||||
"Nov",
|
"Nov",
|
||||||
"Dec",
|
"Dec",
|
||||||
"{weekday} {day} {month}",
|
"{weekday_min} {day} {month}",
|
||||||
"{weekday} {day} {month}",
|
"{weekday} {day} {month}",
|
||||||
"{day} {month}",
|
"{day} {month}",
|
||||||
"{weekday} {time}",
|
"{weekday} {time}",
|
||||||
|
|||||||
+49
-2
@@ -87,6 +87,9 @@ def check():
|
|||||||
problems, notes = [], []
|
problems, notes = [], []
|
||||||
base = dict(gen.flatten({k: v for k, v in english.items() if k != '_meta'}))
|
base = dict(gen.flatten({k: v for k, v in english.items() if k != '_meta'}))
|
||||||
drawable = font_letters()
|
drawable = font_letters()
|
||||||
|
width = text_width()
|
||||||
|
if width is None:
|
||||||
|
notes.append('Pillow is missing: tile texts were not measured')
|
||||||
for code, data in langs.items():
|
for code, data in langs.items():
|
||||||
meta = data.get('_meta', {})
|
meta = data.get('_meta', {})
|
||||||
for field in ('name', 'english', 'script', 'plural', 'checked'):
|
for field in ('name', 'english', 'script', 'plural', 'checked'):
|
||||||
@@ -105,7 +108,10 @@ def check():
|
|||||||
if key not in base or not isinstance(text, str):
|
if key not in base or not isinstance(text, str):
|
||||||
continue
|
continue
|
||||||
source = base[key]
|
source = base[key]
|
||||||
if set(PLACEHOLDER.findall(text)) != set(PLACEHOLDER.findall(source)):
|
if key in EITHER_PLACEHOLDER:
|
||||||
|
if not set(PLACEHOLDER.findall(text)) <= EITHER_PLACEHOLDER[key]:
|
||||||
|
problems.append(f'{code}: {key} has placeholders beyond {sorted(EITHER_PLACEHOLDER[key])}')
|
||||||
|
elif set(PLACEHOLDER.findall(text)) != set(PLACEHOLDER.findall(source)):
|
||||||
problems.append(f'{code}: {key} has placeholders {sorted(set(PLACEHOLDER.findall(text)))}, '
|
problems.append(f'{code}: {key} has placeholders {sorted(set(PLACEHOLDER.findall(text)))}, '
|
||||||
f'English {sorted(set(PLACEHOLDER.findall(source)))}')
|
f'English {sorted(set(PLACEHOLDER.findall(source)))}')
|
||||||
plural = ' | ' in source
|
plural = ' | ' in source
|
||||||
@@ -126,6 +132,14 @@ def check():
|
|||||||
ours = not key.startswith(('screen.ha.', 'screen.date.', 'screen.number.'))
|
ours = not key.startswith(('screen.ha.', 'screen.date.', 'screen.number.'))
|
||||||
if ours and code != 'en' and len(text) > max(8, len(source) * 1.6) and not plural:
|
if ours and code != 'en' and len(text) > max(8, len(source) * 1.6) and not plural:
|
||||||
notes.append(f'{code}: {key} is much longer than the English ({len(text)} vs {len(source)})')
|
notes.append(f'{code}: {key} is much longer than the English ({len(text)} vs {len(source)})')
|
||||||
|
if width and key in TILE_LINES:
|
||||||
|
for board, (size, room) in TILE_LINES[key].items():
|
||||||
|
if width(size, text) > room:
|
||||||
|
problems.append(f'{code}: {key} "{text}" is too wide for a tile on the {board} '
|
||||||
|
f'({width(size, text):.0f} of {room} px); make it shorter')
|
||||||
|
# The weather columns of the CYD hold a day of two letters beside its icon.
|
||||||
|
if key.startswith('screen.date.weekdays_min.') and len(text) > 2:
|
||||||
|
problems.append(f'{code}: {key} "{text}" is longer than two letters; the CYD\'s weather columns cut it')
|
||||||
for line in notes:
|
for line in notes:
|
||||||
print('note:', line)
|
print('note:', line)
|
||||||
for line in problems:
|
for line in problems:
|
||||||
@@ -134,6 +148,34 @@ def check():
|
|||||||
return 1 if problems else 0
|
return 1 if problems else 0
|
||||||
|
|
||||||
|
|
||||||
|
# One line under a tile's name, and the room it has there: Roboto 400 at the CYD's 11 px in about 88 px, at the
|
||||||
|
# Guition's 16 px in about 128 px (packages/boards: TILE_W, FONT_SUBLABEL_SIZE). A longer text is cut; `check` fails.
|
||||||
|
TILE_ROOM = {'CYD': (11, 88), 'Guition': (16, 128)}
|
||||||
|
# Camera tiles are the Guition's alone.
|
||||||
|
TILE_LINES = {'screen.tile.tap_to_open': TILE_ROOM, 'screen.script.never_run': TILE_ROOM, 'screen.script.running': TILE_ROOM,
|
||||||
|
'screen.media.not_playing': TILE_ROOM, 'screen.timer.paused': TILE_ROOM,
|
||||||
|
'screen.camera.tap_to_view': {'Guition': TILE_ROOM['Guition']},
|
||||||
|
'screen.camera.no_image_yet': {'Guition': TILE_ROOM['Guition']}}
|
||||||
|
|
||||||
|
|
||||||
|
def text_width():
|
||||||
|
"""(size, text) -> pixels in the screens' Roboto 400, or None without Pillow (the check then skips widths)."""
|
||||||
|
try:
|
||||||
|
from PIL import ImageFont
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
fonts = {}
|
||||||
|
def width(size, text):
|
||||||
|
if size not in fonts:
|
||||||
|
fonts[size] = ImageFont.truetype(str(ROOT / 'fonts' / 'Roboto-400.ttf'), size)
|
||||||
|
return fonts[size].getlength(text)
|
||||||
|
return width
|
||||||
|
|
||||||
|
|
||||||
|
# Texts whose placeholders a language picks from a set: the top bar's date takes the weekday's abbreviation ({weekday},
|
||||||
|
# "sam.") or its two letters ({weekday_min}, "Sa" in English).
|
||||||
|
EITHER_PLACEHOLDER = {'screen.date.top_bar': {'weekday', 'weekday_min', 'day', 'month'}}
|
||||||
|
|
||||||
# English that stays in the firmware on purpose, with why. Everything else a screen shows comes from the translations,
|
# English that stays in the firmware on purpose, with why. Everything else a screen shows comes from the translations,
|
||||||
# and `lint` fails on a new English text in the code, so it never slips back in.
|
# and `lint` fails on a new English text in the code, so it never slips back in.
|
||||||
LINT_KEEP = {
|
LINT_KEEP = {
|
||||||
@@ -397,7 +439,12 @@ def cldr_dates(write, codes=None):
|
|||||||
before = json.dumps(data, ensure_ascii=False)
|
before = json.dumps(data, ensure_ascii=False)
|
||||||
data.setdefault('_meta', {})['clock'] = cldr['clock']
|
data.setdefault('_meta', {})['clock'] = cldr['clock']
|
||||||
number = {**cldr['number'], 'percent': ' %' if percent_space(code) else '%'}
|
number = {**cldr['number'], 'percent': ' %' if percent_space(code) else '%'}
|
||||||
merge(data.setdefault('screen', {}), {'number': number, 'time': cldr['time'], 'date': cldr['date']})
|
date = dict(cldr['date'])
|
||||||
|
# The two letters of the weather columns are the language's own choice once it has them (the CLDR's short
|
||||||
|
# width, which Node's Intl doesn't give): only a new language starts from the abbreviation.
|
||||||
|
if data.get('screen', {}).get('date', {}).get('weekdays_min'):
|
||||||
|
date.pop('weekdays_min')
|
||||||
|
merge(data.setdefault('screen', {}), {'number': number, 'time': cldr['time'], 'date': date})
|
||||||
if json.dumps(data, ensure_ascii=False) != before:
|
if json.dumps(data, ensure_ascii=False) != before:
|
||||||
print(f'{code}: {"wrote" if write else "would change"} the CLDR texts')
|
print(f'{code}: {"wrote" if write else "would change"} the CLDR texts')
|
||||||
if write:
|
if write:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { HeaderItem } from "../types";
|
|||||||
const query = ref("");
|
const query = ref("");
|
||||||
const taken = computed(() => new Set(topbarItems().map(itemKey)));
|
const taken = computed(() => new Set(topbarItems().map(itemKey)));
|
||||||
const samples = computed(() => ({
|
const samples = computed(() => ({
|
||||||
clock: clockText(clock24.value, new Date(state.now)),
|
clock: clockText(clock24.value, new Date(state.now), screenLanguage.value),
|
||||||
analog: t("editor.topbar.analog_sample"),
|
analog: t("editor.topbar.analog_sample"),
|
||||||
date: dateText(new Date(state.now), screenLanguage.value),
|
date: dateText(new Date(state.now), screenLanguage.value),
|
||||||
} as Record<string, string>));
|
} as Record<string, string>));
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const liveNote = computed(() => {
|
|||||||
return !view.shown ? t("editor.topbar.live.hidden") : overflow.value.has(props.index) ? t("editor.topbar.live.overflow") : t("editor.topbar.live.looks");
|
return !view.shown ? t("editor.topbar.live.hidden") : overflow.value.has(props.index) ? t("editor.topbar.live.overflow") : t("editor.topbar.live.looks");
|
||||||
});
|
});
|
||||||
const samples = computed(() => ({
|
const samples = computed(() => ({
|
||||||
clock: clockText(clock24.value, new Date(state.now)),
|
clock: clockText(clock24.value, new Date(state.now), screenLanguage.value),
|
||||||
analog: t("editor.topbar.analog_sample"),
|
analog: t("editor.topbar.analog_sample"),
|
||||||
date: dateText(new Date(state.now), screenLanguage.value),
|
date: dateText(new Date(state.now), screenLanguage.value),
|
||||||
} as Record<string, string>));
|
} as Record<string, string>));
|
||||||
|
|||||||
+10
-6
@@ -12,14 +12,18 @@ export const BUILTIN_ICONS: Record<string, string> = { clock: "clock-outline", a
|
|||||||
export const itemKey = (item: HeaderItem) => JSON.stringify(item);
|
export const itemKey = (item: HeaderItem) => JSON.stringify(item);
|
||||||
export const glyph = (cp: string) => String.fromCodePoint(parseInt(cp, 16));
|
export const glyph = (cp: string) => String.fromCodePoint(parseInt(cp, 16));
|
||||||
|
|
||||||
export function clockText(clock24: boolean, now = new Date()) {
|
// The time as the screens write it (screen_text::clock_text): "07:12" on 24 hours; on 12 "7:12 PM" in the top bar, with
|
||||||
let hours = now.getHours();
|
// the language's day periods, and "7:12" in the clock card's big digits, which have no letters.
|
||||||
// The screen formats with %I:%M when the 24-hour clock is off.
|
export function clockText(clock24: boolean, now = new Date(), locale?: string) {
|
||||||
if (!clock24) hours = hours % 12 || 12;
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||||
return `${String(hours).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
if (clock24) return `${String(now.getHours()).padStart(2, "0")}:${minutes}`;
|
||||||
|
const time = `${now.getHours() % 12 || 12}:${minutes}`;
|
||||||
|
return locale ? `${time} ${t(`screen.time.${now.getHours() < 12 ? "am" : "pm"}`, {}, { locale })}` : time;
|
||||||
}
|
}
|
||||||
|
// "Sa 19 Sep", "za 19 sep", "sam. 19 sept.": the language's pattern with its abbreviation or its two letters.
|
||||||
export const dateText = (now = new Date(), locale = "en") => t("screen.date.top_bar", {
|
export const dateText = (now = new Date(), locale = "en") => t("screen.date.top_bar", {
|
||||||
weekday: t(`screen.date.weekdays_min.${now.getDay()}`, {}, { locale }),
|
weekday: t(`screen.date.weekdays_short.${now.getDay()}`, {}, { locale }),
|
||||||
|
weekday_min: t(`screen.date.weekdays_min.${now.getDay()}`, {}, { locale }),
|
||||||
day: now.getDate(),
|
day: now.getDate(),
|
||||||
month: t(`screen.date.months_short.${now.getMonth()}`, {}, { locale }),
|
month: t(`screen.date.months_short.${now.getMonth()}`, {}, { locale }),
|
||||||
}, { locale });
|
}, { locale });
|
||||||
|
|||||||
+1
-1
@@ -583,7 +583,7 @@ export function topbarLabel(item: HeaderItem) {
|
|||||||
// What the item shows right now: { icon, text, color, shown }. Entities wait for the add-on's preview.
|
// What the item shows right now: { icon, text, color, shown }. Entities wait for the add-on's preview.
|
||||||
export function topbarView(item: HeaderItem): ItemView {
|
export function topbarView(item: HeaderItem): ItemView {
|
||||||
const now = new Date(state.now);
|
const now = new Date(state.now);
|
||||||
if (item.type === "clock") return { text: clockText(clock24.value, now), shown: true };
|
if (item.type === "clock") return { text: clockText(clock24.value, now, screenLanguage.value), shown: true };
|
||||||
if (item.type === "date") return { text: dateText(now, screenLanguage.value), shown: true };
|
if (item.type === "date") return { text: dateText(now, screenLanguage.value), shown: true };
|
||||||
if (item.type === "analog") return { analog: true, shown: true };
|
if (item.type === "analog") return { analog: true, shown: true };
|
||||||
const p = state.topbarPreviews[itemKey(item)];
|
const p = state.topbarPreviews[itemKey(item)];
|
||||||
|
|||||||
@@ -175,7 +175,8 @@ describe("the mockup speaks the screens' language", () => {
|
|||||||
ha: { unavailable: "Nicht verfügbar", on: "An", weather: { windy: "Windig" }, binary: { door_on: "Offen" }, button: { run: "Ausführen" } },
|
ha: { unavailable: "Nicht verfügbar", on: "An", weather: { windy: "Windig" }, binary: { door_on: "Offen" }, button: { run: "Ausführen" } },
|
||||||
tile: { page: "Seite {n}" },
|
tile: { page: "Seite {n}" },
|
||||||
time: { hours_ago: "Vor 1 Stunde | Vor {n} Stunden", just_now: "Gerade eben" },
|
time: { hours_ago: "Vor 1 Stunde | Vor {n} Stunden", just_now: "Gerade eben" },
|
||||||
date: { weekdays_min: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], months_short: ["Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez."], top_bar: "{weekday}., {day}. {month}" },
|
date: { weekdays_min: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], weekdays_short: ["So.", "Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa."],
|
||||||
|
months_short: ["Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez."], top_bar: "{weekday}, {day}. {month}" },
|
||||||
number: { decimal: ",", group: ".", group_min: "2" },
|
number: { decimal: ",", group: ".", group_min: "2" },
|
||||||
},
|
},
|
||||||
}, meta("one_other"));
|
}, meta("one_other"));
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ describe("clock and date", () => {
|
|||||||
const evening = new Date(2026, 8, 15, 19, 5);
|
const evening = new Date(2026, 8, 15, 19, 5);
|
||||||
it("formats the clock the way the screen does", () => {
|
it("formats the clock the way the screen does", () => {
|
||||||
expect(clockText(true, evening)).toBe("19:05");
|
expect(clockText(true, evening)).toBe("19:05");
|
||||||
expect(clockText(false, evening)).toBe("07:05");
|
// The clock card's big digits: no leading zero, no day period (its font has no letters).
|
||||||
|
expect(clockText(false, evening)).toBe("7:05");
|
||||||
expect(clockText(false, new Date(2026, 8, 15, 0, 30))).toBe("12:30");
|
expect(clockText(false, new Date(2026, 8, 15, 0, 30))).toBe("12:30");
|
||||||
|
// The top bar: with the language's day period, as screen_text::clock_text writes it.
|
||||||
|
expect(clockText(false, evening, "en")).toBe("7:05 PM");
|
||||||
|
expect(clockText(false, new Date(2026, 8, 15, 0, 30), "en")).toBe("12:30 AM");
|
||||||
|
expect(clockText(true, evening, "en")).toBe("19:05");
|
||||||
});
|
});
|
||||||
it("writes the date short", () => {
|
it("writes the date short", () => {
|
||||||
expect(dateText(evening)).toBe("Tu 15 Sep");
|
expect(dateText(evening)).toBe("Tu 15 Sep");
|
||||||
|
|||||||
Reference in New Issue
Block a user