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:
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -156,7 +156,8 @@ export function effectiveControls(tile: Tile, inventory: Inventory): string | nu
|
||||
export function controlsLabel(tile: Tile, inventory: Inventory) {
|
||||
const key = effectiveControls(tile, inventory);
|
||||
if (!key) return t("editor.inspect.control_none");
|
||||
return inventory.controls?.[tile.entity.split(".")[0]]?.choices.find((c) => c.key === key)?.label.toLocaleLowerCase() || key;
|
||||
// As the choice itself is labelled, like the other values in the summary: German writes its nouns with a capital.
|
||||
return inventory.controls?.[tile.entity.split(".")[0]]?.choices.find((c) => c.key === key)?.label || key;
|
||||
}
|
||||
|
||||
export const parseVersion = (v: string | undefined | null) => (/^(\d+)\.(\d+)\.(\d+)$/.exec(v || "") || []).slice(1).map(Number);
|
||||
|
||||
+19
-6
@@ -2,7 +2,7 @@
|
||||
// former app.js state and its calls, with the DOM work moved into the components.
|
||||
import { computed, reactive, toRaw, watch } from "vue";
|
||||
import { api, getJson, send, setCsrf } from "./api";
|
||||
import { andList, editorLanguage, languageMarks, languageMeta, loadLanguage, type NumberMarks, pickLanguage, STYLE_MARKS, t } from "./i18n";
|
||||
import { andList, editorLanguage, languageMeta, loadLanguage, type NumberMarks, pickLanguage, STYLE_MARKS, t } from "./i18n";
|
||||
import {
|
||||
arrange, cellsOf, entriesOf, firstFree, fits, isFull, isWide, MAX_PAGES, nearestFree, newTile, normalize, occupied, pageCount, pageOf,
|
||||
pageTarget, rowStart, sizeOf, SLOTS_PER_PAGE, strandedPages, supportsFirmware as supportsVersion, tileLimit as limitFor,
|
||||
@@ -832,14 +832,27 @@ export const languageName = (code: string | null | undefined) =>
|
||||
export const needsUpdate = (screen: Screen) => Boolean(screen.update?.available || screen.update?.language);
|
||||
export const newLanguageText = () => t("editor.update.new_language", { name: languageName(state.inventory.language?.effective) });
|
||||
// Time and number format, for every screen at once under Settings → Language & region: a 24-hour clock and "1,234.5"
|
||||
// until the add-on says otherwise. The mockup's clocks and numbers follow them; "auto" numbers are the screens'
|
||||
// language's own (screen.number), as the firmware writes them.
|
||||
// until the add-on says otherwise. The mockup's clocks and numbers follow what the add-on sends the screens: the style,
|
||||
// from how many digits a number is grouped, and the space before "%" (Home Assistant's language decides "auto").
|
||||
export const clock24 = computed(() => state.inventory.language?.clock_effective !== "12");
|
||||
export const screenLanguageMarks = computed<NumberMarks>(() => languageMarks(screenLanguage.value));
|
||||
export const numberMarks = computed<NumberMarks>(() => {
|
||||
const choice = state.inventory.language ? state.inventory.language.numbers || "auto" : "point";
|
||||
return choice === "auto" ? screenLanguageMarks.value : STYLE_MARKS[choice];
|
||||
const language = state.inventory.language;
|
||||
const marks = STYLE_MARKS[language?.numbers_effective || "point"] || STYLE_MARKS.point;
|
||||
return { ...marks, from: (language?.group_min || 1) >= 2 ? 5 : 4 };
|
||||
});
|
||||
/** How Automatic writes numbers: the marks of the language that decides, for the label of that choice. */
|
||||
export const autoMarks = computed<NumberMarks>(() => {
|
||||
const language = state.inventory.language;
|
||||
return { ...(STYLE_MARKS[language?.numbers_auto || "point"] || STYLE_MARKS.point), from: (language?.group_min_auto || 1) >= 2 ? 5 : 4 };
|
||||
});
|
||||
/** What follows a number for its unit, as Home Assistant spaces it: "°", "%" or " %" by the language, " kWh". */
|
||||
export function unitSuffix(unit: string | undefined | null) {
|
||||
if (!unit || unit === "°") return unit || "";
|
||||
if (unit === "%") return state.inventory.language?.percent_space ? " %" : "%";
|
||||
return ` ${unit}`;
|
||||
}
|
||||
/** A built-in card's name as the screens show it (Settings, Clock, Go to page 2), in their language. */
|
||||
export const screenBuiltinName = (id: string) => state.inventory.builtin?.find((e) => e.id === id)?.screen_name;
|
||||
/** Saves any of the screen language, the time format and the number format. */
|
||||
export async function saveLanguage(changes: { setting?: string; clock?: string; numbers?: string }) {
|
||||
try {
|
||||
|
||||
+5
-1
@@ -45,9 +45,13 @@ export type Languages = {
|
||||
setting: string; effective: string; ha: string | null; languages: LanguageInfo[];
|
||||
clock?: "auto" | "24" | "12"; clock_effective?: "24" | "12";
|
||||
numbers?: "auto" | "point" | "comma" | "space"; numbers_effective?: "point" | "comma" | "space";
|
||||
/** From how many digits a whole number gets separators: 2 is 1234 but 12.345 (CLDR); a space before "%". */
|
||||
group_min?: number; percent_space?: boolean;
|
||||
/** What Automatic means now: the clock and numbers of Home Assistant's language (or the chosen one). */
|
||||
clock_auto?: "24" | "12"; numbers_auto?: "point" | "comma" | "space"; group_min_auto?: number;
|
||||
};
|
||||
export type ChangelogSection = { app: string; firmware: string; lines: string[] };
|
||||
export type Entity = { id: string; name: string; area?: string; device?: string; icon?: string; state?: string; tile?: boolean };
|
||||
export type Entity = { id: string; name: string; area?: string; device?: string; icon?: string; state?: string; tile?: boolean; screen_name?: string };
|
||||
export type IconInfo = { name: string; cp: string; label: string };
|
||||
export type Inventory = {
|
||||
csrf?: string;
|
||||
|
||||
Reference in New Issue
Block a user