The editor's mockup writes like the screens, and speaks its own language around them

- Numbers in the mockup follow what the app sends the screens (style, grouping from five digits, the space before
  %), a state without a unit stays as it is, and built-in cards carry the screens' names (Instellingen, not the
  editor's Einstellungen).
- Automatic in Language & region is labelled with what it means now (Home Assistant's language decides).
- A screen's status (Synced, Loading tiles, ...) shows in the editor's language; the screens keep reporting it in
  English, which the app reads.
- The doorbell example uses the alert examples of the editor's language; control labels keep their capitals.
- Editor bundle rebuilt with every language's texts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-19 16:57:35 +02:00
parent 125346d94e
commit 365ed158b9
37 changed files with 217 additions and 73 deletions
+7 -5
View File
@@ -47,16 +47,18 @@ const exampleYaml = computed(() => {
const lines = (alerts.value?.fields || []).map((f: any) => ` ${f.name}: ${fieldValue(f)}`);
return `action: ${chosenAction.value || "esphome.<device_name>_show_alert"}\ndata:\n${lines.join("\n")}`;
});
// The doorbell example in the editor's language: the fields' own examples, and two lines of its own.
const example = (name: string, fallback: string) => yamlString((alerts.value?.fields || []).find((f: any) => f.name === name)?.example || fallback);
const waitYaml = computed(() => [
`# Doorbell: show the alert and wait until someone presses the button.`,
`# ${t("editor.alerts.wait_yaml.comment")}`,
`actions:`,
` - action: ${chosenAction.value || "esphome.<device_name>_show_alert"}`,
` data:`,
` title: "Someone is at the door"`,
` subtitle: "Door 3, back"`,
` title: ${example("title", "Someone is at the door")}`,
` subtitle: ${example("subtitle", "Door 3, back")}`,
` icon: doorbell`,
` color: orange`,
` button_text: "Coming"`,
` button_text: ${example("button_text", "Coming")}`,
` timeout: 0`,
` flash: true`,
` - wait_for_trigger:`,
@@ -71,7 +73,7 @@ const waitYaml = computed(() => [
` then:`,
` - action: notify.notify`,
` data:`,
` message: "Someone is coming to the door."`,
` message: ${yamlString(t("editor.alerts.wait_yaml.message"))}`,
].join("\n"));
// One event for every screen (app 0.2.45): an action for "Edit in YAML" of the Event action.
const allYaml = computed(() => {
+3 -3
View File
@@ -2,7 +2,7 @@
// Everything around the screens: firmware updates, language and region, alerts, Claude.
import { computed, ref } from "vue";
import { haProfile, matchLanguage, numberText, type NumberMarks, type NumberStyle, STYLE_MARKS, t } from "../i18n";
import { anyUpdating, go, installClaudeSkill, runUpdateAll, saveLanguage, screenLanguageMarks, setAutoUpdate, state, updateProgress } from "../store";
import { anyUpdating, autoMarks, go, installClaudeSkill, runUpdateAll, saveLanguage, setAutoUpdate, state, updateProgress } from "../store";
const u = computed(() => state.inventory.updates);
const outdated = computed(() => state.inventory.screens.filter((s) => s.update?.available).length);
@@ -125,7 +125,7 @@ async function useProfile() {
<div class="field">
<label class="f-label" for="time-format">{{ t("editor.settings.language.time") }}</label>
<select id="time-format" :value="lang.clock || 'auto'" :disabled="saving" @change="choose('clock', $event)">
<option value="auto">{{ t("editor.settings.language.time_auto", { clock: clockName(lang.clock_effective) }) }}</option>
<option value="auto">{{ t("editor.settings.language.time_auto", { clock: clockName(lang.clock_auto || lang.clock_effective) }) }}</option>
<option value="24">{{ clockName("24") }}</option>
<option value="12">{{ clockName("12") }}</option>
</select>
@@ -133,7 +133,7 @@ async function useProfile() {
<div class="field">
<label class="f-label" for="number-format">{{ t("editor.settings.language.numbers") }}</label>
<select id="number-format" :value="lang.numbers || 'auto'" :disabled="saving" @change="choose('numbers', $event)">
<option value="auto">{{ t("editor.settings.language.numbers_auto", { example: example(screenLanguageMarks) }) }}</option>
<option value="auto">{{ t("editor.settings.language.numbers_auto", { example: example(autoMarks) }) }}</option>
<option v-for="style in STYLES" :key="style" :value="style">{{ example(STYLE_MARKS[style]) }}</option>
</select>
</div>
+11 -6
View File
@@ -6,11 +6,12 @@ import { vDrag } from "../drag";
import { numberText, t, te } from "../i18n";
import { displayName, effectiveControls, isFull, isWide, pageOf, pageTarget, SLOTS_PER_PAGE } from "../model/layout";
import { clockText, glyph } from "../model/topbar";
import { clock24, entityName, isSelected, liveOf, numberMarks, openTile, placeTile, removeTile, screenText, state, tileIconCp } from "../store";
import { clock24, entityName, isSelected, liveOf, numberMarks, openTile, placeTile, removeTile, screenBuiltinName, screenText, state, tileIconCp, unitSuffix } from "../store";
import type { Tile } from "../types";
const props = defineProps<{ tile: Tile; slot: number; placeholder?: boolean }>();
const name = computed(() => props.tile.name || entityName(props.tile.entity));
// A built-in card is named as the screens name it, in their language (app 0.2.90).
const name = computed(() => props.tile.name || (domain.value === "screen" && screenBuiltinName(props.tile.entity)) || entityName(props.tile.entity));
const full = computed(() => isFull(props.tile));
const wide = computed(() => isWide(props.tile) && !full.value);
const goesTo = computed(() => pageTarget(props.tile.entity));
@@ -36,8 +37,11 @@ const on = computed(() => Boolean(current.value) && !gone.value && current.value
const isOn = computed(() => ["light", "switch", "input_boolean", "fan"].includes(domain.value) && current.value?.state === "on");
const unit = computed(() => current.value?.a?.unit_of_measurement as string | undefined);
const capital = (text: string) => text.charAt(0).toUpperCase() + text.slice(1).replace(/_/g, " ");
// Numbers as the screens write them, "1,234.5" or "1.234,5" (app 0.2.90).
// Numbers as the screens write them, "1,234.5" or "1.234,5" (app 0.2.90): a state only with a unit, or of an entity
// that is a number itself, as the firmware does (value_text); an id-like "1234" without a unit stays as it is.
const num = (value: unknown) => numberText(value as string | number, numberMarks.value);
const NUMERIC = ["number", "input_number", "counter"];
const value = (state: string) => (unit.value || NUMERIC.includes(domain.value) ? `${num(state)}${unitSuffix(unit.value)}` : state);
// The screens' own words for a state where Home Assistant hands us none (screen.ha, Home Assistant's words in the
// screens' language, app 0.2.90): a binary sensor's by its device class, on and off, and the states of the domains
// the screen names itself. A weather's windy-variant is windy there too.
@@ -60,12 +64,13 @@ const status = computed(() => {
const word = c.word || haWord(c) || capital(c.state);
if (domain.value === "climate") return `${a.current_temperature !== undefined ? `${num(a.current_temperature)}° · ` : ""}${word}`;
if (domain.value === "weather") return `${word}${a.temperature !== undefined ? ` · ${num(a.temperature)}°` : ""}`;
if (domain.value === "cover" && a.current_position !== undefined && a.current_position > 0 && a.current_position < 100) return `${word} · ${a.current_position} %`;
if (domain.value === "cover" && a.current_position !== undefined && a.current_position > 0 && a.current_position < 100) return `${word} · ${a.current_position}${unitSuffix("%")}`;
if (domain.value === "media_player" && a.media_title) return `${word} · ${a.media_title}`;
if (domain.value === "sensor") return `${num(c.state)}${unit.value ? ` ${unit.value}` : ""}`;
if (domain.value === "sensor" || NUMERIC.includes(domain.value)) return value(c.state);
return word;
});
const bigValue = computed(() => (current.value && !gone.value ? num(current.value.state) : "—"));
// The big value of the watch display; its unit sits beside it in small letters.
const bigValue = computed(() => (current.value && !gone.value ? (unit.value || NUMERIC.includes(domain.value) ? num(current.value.state) : current.value.state) : "—"));
// The small slider's fill, from what the entity reports; off is empty, like the screen's grey fill.
const fill = computed(() => {
const c = current.value;