App Updates

This commit is contained in:
2026-07-21 11:19:17 -04:00
parent 87ffe648a5
commit 0ed1687503
180 changed files with 943 additions and 118 deletions
@@ -0,0 +1,52 @@
/** #106 follow-up: reproduce with the EXACT _build_task_summary shape. */
import { expect, fixture, html } from "@open-wc/testing";
import "../components/task-dialog.js";
import type { MaintenanceTaskDialog } from "../components/task-dialog";
import { createMockHass } from "./_test-utils.js";
it("fleet-shaped task (nested manual schedule + plural-only threshold) keeps its trigger", async () => {
const { hass, sent } = createMockHass({
states: { "sensor.maintenance_supporter_batteries_to_replace": { state: "8" } },
handlers: { "maintenance_supporter/task/update": () => ({ success: true }) },
});
const el = await fixture<MaintenanceTaskDialog>(html`
<maintenance-task-dialog .hass=${hass}></maintenance-task-dialog>
`);
await el.updateComplete;
await el.openEdit("entry_x", {
id: "t1",
name: "Replace low batteries",
type: "inspection",
enabled: true,
schedule_type: "sensor_based",
interval_days: null,
interval_unit: "days",
due_date: null,
interval_anchor: "completion",
schedule: { kind: "manual" },
warning_days: 7,
battery_fleet_task: true,
trigger_config: {
type: "threshold",
entity_ids: ["sensor.maintenance_supporter_batteries_to_replace"],
trigger_above: 0,
entity_logic: "any",
auto_complete_on_recovery: true,
},
trigger_entity_info: {
entity_id: "sensor.maintenance_supporter_batteries_to_replace",
friendly_name: "Batteries to replace",
unit_of_measurement: null,
},
} as any);
await el.updateComplete;
// the user's edit: rename + warning days
(el as any)._name = "Remplacer les piles";
(el as any)._warningDays = "1";
await (el as any)._save();
const update = sent.find((m: any) => m.type === "maintenance_supporter/task/update") as any;
expect(update, "update sent").to.exist;
expect(update.trigger_config, "trigger_config must survive").to.exist;
expect(update.trigger_config.type).to.equal("threshold");
expect(update.trigger_config.trigger_above).to.equal(0);
});
@@ -84,6 +84,7 @@ export class MaintenanceBatteryFleetSection extends LitElement {
try {
await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/battery_fleet/setup",
language: this._lang,
});
await this._load();
} catch (e) {
@@ -1045,6 +1045,7 @@ export class MaintenanceSupporterPanel extends LitElement {
try {
const res = await this.hass.connection.sendMessagePromise<{ entry_id: string; task_id?: string }>({
type: "maintenance_supporter/battery_fleet/setup",
language: this.hass.language || "en",
});
this._batteryFleetSetupAvailable = false;
await this._loadData();
@@ -1235,6 +1235,10 @@ export const panelStyles = css`
:host([narrow]) .task-header-actions {
width: 100%;
justify-content: flex-start;
/* Longer labels (de "Überspringen", fr "Archiver", ) overflow a phone
viewport in a nowrap row the menu then needs a horizontal scroll
to reach. Wrap instead; language-independent. */
flex-wrap: wrap;
}
:host([narrow]) .filter-bar {
@@ -1433,7 +1437,7 @@ export const panelStyles = css`
.two-column-layout { grid-template-columns: 1fr; }
.tab { padding: 12px 16px; font-size: 14px; }
.task-header { flex-direction: column; align-items: flex-start; }
.task-header-actions { width: 100%; justify-content: flex-start; }
.task-header-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
.filter-bar { flex-wrap: wrap; }
.filter-bar select { flex: 1; min-width: 0; }
/* Mirror the :host([narrow]) grid layout for narrow desktop windows */
@@ -2528,6 +2528,10 @@ ${H(n.notes)}</div>`:""}
:host([narrow]) .task-header-actions {
width: 100%;
justify-content: flex-start;
/* Longer labels (de "Überspringen", fr "Archiver", ) overflow a phone
viewport in a nowrap row the menu then needs a horizontal scroll
to reach. Wrap instead; language-independent. */
flex-wrap: wrap;
}
:host([narrow]) .filter-bar {
@@ -2726,7 +2730,7 @@ ${H(n.notes)}</div>`:""}
.two-column-layout { grid-template-columns: 1fr; }
.tab { padding: 12px 16px; font-size: 14px; }
.task-header { flex-direction: column; align-items: flex-start; }
.task-header-actions { width: 100%; justify-content: flex-start; }
.task-header-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
.filter-bar { flex-wrap: wrap; }
.filter-bar select { flex: 1; min-width: 0; }
/* Mirror the :host([narrow]) grid layout for narrow desktop windows */
@@ -5354,7 +5358,7 @@ ${p?`<div class="sub">${p}</div>`:""}
color: var(--primary-text-color);
}
.actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; }
`,d([y({attribute:!1})],X.prototype,"hass",2),d([_()],X.prototype,"_open",2),d([_()],X.prototype,"_loading",2),d([_()],X.prototype,"_adopting",2),d([_()],X.prototype,"_error",2),d([_()],X.prototype,"_setups",2),d([_()],X.prototype,"_selected",2),d([_()],X.prototype,"_baselines",2),d([_()],X.prototype,"_targets",2),d([_()],X.prototype,"_objects",2);customElements.get("maintenance-suggested-setups-dialog")||customElements.define("maintenance-suggested-setups-dialog",X);var ve=class extends S{constructor(){super(...arguments);this._ov=null;this._loading=!1;this._marking=!1;this._error="";this._localeReady=!1;this._markAll=async()=>{await this._mark(void 0)};this._repair=async()=>{if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup"}),await this._load()}catch(e){this._error=j(e,this._lang)}finally{this._marking=!1}}}}get _lang(){return this.hass?.language||"en"}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(e){e.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,V(this._lang).then(()=>this.requestUpdate()),this._ov===null&&!this._loading&&this._load())}async _load(){this._loading=!0,this._error="";try{this._ov=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview"})}catch(e){this._error=j(e,this._lang)}finally{this._loading=!1}}async _mark(e){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/mark_replaced",...e?{entity_ids:e}:{}}),await this._load()}catch(t){this._error=j(t,this._lang)}finally{this._marking=!1}}}_shoppingLine(e){return Object.entries(e).map(([t,i])=>`${i}\xD7 ${t}`).join(" \xB7 ")}render(){let e=this._lang;if(this._loading&&this._ov===null)return o`<div class="bf-card"><div class="bf-loading"></div></div>`;let t=this._ov;if(!t)return this._error?o`<div class="bf-card"><div class="bf-error">${this._error}</div></div>`:h;let i=t.low.length;return o`
`,d([y({attribute:!1})],X.prototype,"hass",2),d([_()],X.prototype,"_open",2),d([_()],X.prototype,"_loading",2),d([_()],X.prototype,"_adopting",2),d([_()],X.prototype,"_error",2),d([_()],X.prototype,"_setups",2),d([_()],X.prototype,"_selected",2),d([_()],X.prototype,"_baselines",2),d([_()],X.prototype,"_targets",2),d([_()],X.prototype,"_objects",2);customElements.get("maintenance-suggested-setups-dialog")||customElements.define("maintenance-suggested-setups-dialog",X);var ve=class extends S{constructor(){super(...arguments);this._ov=null;this._loading=!1;this._marking=!1;this._error="";this._localeReady=!1;this._markAll=async()=>{await this._mark(void 0)};this._repair=async()=>{if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup",language:this._lang}),await this._load()}catch(e){this._error=j(e,this._lang)}finally{this._marking=!1}}}}get _lang(){return this.hass?.language||"en"}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(e){e.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,V(this._lang).then(()=>this.requestUpdate()),this._ov===null&&!this._loading&&this._load())}async _load(){this._loading=!0,this._error="";try{this._ov=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview"})}catch(e){this._error=j(e,this._lang)}finally{this._loading=!1}}async _mark(e){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/mark_replaced",...e?{entity_ids:e}:{}}),await this._load()}catch(t){this._error=j(t,this._lang)}finally{this._marking=!1}}}_shoppingLine(e){return Object.entries(e).map(([t,i])=>`${i}\xD7 ${t}`).join(" \xB7 ")}render(){let e=this._lang;if(this._loading&&this._ov===null)return o`<div class="bf-card"><div class="bf-loading"></div></div>`;let t=this._ov;if(!t)return this._error?o`<div class="bf-card"><div class="bf-error">${this._error}</div></div>`:h;let i=t.low.length;return o`
<div class="bf-card">
<div class="bf-head">
<ha-icon icon="mdi:battery-alert"></ha-icon>
@@ -8389,7 +8393,7 @@ ${p?`<div class="sub">${p}</div>`:""}
<div class="palette-hint">${s("palette_hint",e)}</div>
</div>
</div>
`}_openAdoptProblemSensors(){this.shadowRoot.querySelector("maintenance-adopt-problem-sensors-dialog")?.open()}async _onProblemSensorsAdopted(e){let t=e.detail?.tasks_created??0,i=e.detail?.created??[];await this._loadData();let a=s("adopt_problem_done",this._lang).replace("{tasks}",String(t));i.length>0?this._showActionToast(a,s("adopt_problem_configure",this._lang),()=>{let l=i[0],c=this._objects.find(u=>u.entry_id===l.entry_id),p=c?.tasks.find(u=>u.id===l.task_id);c&&p&&this.shadowRoot.querySelector("maintenance-task-dialog")?.openEdit(l.entry_id,p)}):this._showToast(a)}async _setupBatteryFleet(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup"});this._batteryFleetSetupAvailable=!1,await this._loadData();let t=this._objects.find(a=>a.entry_id===e.entry_id),i=t?.tasks.find(a=>a.id===e.task_id)||t?.tasks[0];t&&i&&this._showTask(t.entry_id,i.id),this._showToast(s("battery_fleet_setup_done",this._lang))}catch(e){this._showToast(j(e,this._lang))}}_openSuggestedSetups(){this.shadowRoot.querySelector("maintenance-suggested-setups-dialog")?.open()}_onSetupsAdopted(e){let t=e.detail?.tasks_created??0;this._showToast(s("setups_done",this._lang).replace("{tasks}",String(t))),this._loadData()}async _openTemplateGallery(){if(this._templateGalleryOpen=!0,!(this._templates.length>0))try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/templates",language:this._lang});this._templateCategories=e.categories||{},this._templates=(e.templates||[]).filter(t=>!t.disabled)}catch{this._showToast(s("action_error",this._lang))}}async _createFromTemplate(e){this._templateBusy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/from_template",language:this._lang,template_id:e});this._templateGalleryOpen=!1,await this._loadData(),this._showToast(s("template_created",this._lang)),t?.entry_id&&this._showObject(t.entry_id)}catch{this._showToast(s("action_error",this._lang))}finally{this._templateBusy=!1}}_categoryName(e){let t=this._templateCategories[e];return t&&(t[`name_${this._lang}`]||t.name_en)||e}_renderTemplateGallery(){if(!this._templateGalleryOpen)return h;let e=this._lang,t=new Map;for(let i of this._templates)t.has(i.category)||t.set(i.category,[]),t.get(i.category).push(i);return o`
`}_openAdoptProblemSensors(){this.shadowRoot.querySelector("maintenance-adopt-problem-sensors-dialog")?.open()}async _onProblemSensorsAdopted(e){let t=e.detail?.tasks_created??0,i=e.detail?.created??[];await this._loadData();let a=s("adopt_problem_done",this._lang).replace("{tasks}",String(t));i.length>0?this._showActionToast(a,s("adopt_problem_configure",this._lang),()=>{let l=i[0],c=this._objects.find(u=>u.entry_id===l.entry_id),p=c?.tasks.find(u=>u.id===l.task_id);c&&p&&this.shadowRoot.querySelector("maintenance-task-dialog")?.openEdit(l.entry_id,p)}):this._showToast(a)}async _setupBatteryFleet(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup",language:this.hass.language||"en"});this._batteryFleetSetupAvailable=!1,await this._loadData();let t=this._objects.find(a=>a.entry_id===e.entry_id),i=t?.tasks.find(a=>a.id===e.task_id)||t?.tasks[0];t&&i&&this._showTask(t.entry_id,i.id),this._showToast(s("battery_fleet_setup_done",this._lang))}catch(e){this._showToast(j(e,this._lang))}}_openSuggestedSetups(){this.shadowRoot.querySelector("maintenance-suggested-setups-dialog")?.open()}_onSetupsAdopted(e){let t=e.detail?.tasks_created??0;this._showToast(s("setups_done",this._lang).replace("{tasks}",String(t))),this._loadData()}async _openTemplateGallery(){if(this._templateGalleryOpen=!0,!(this._templates.length>0))try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/templates",language:this._lang});this._templateCategories=e.categories||{},this._templates=(e.templates||[]).filter(t=>!t.disabled)}catch{this._showToast(s("action_error",this._lang))}}async _createFromTemplate(e){this._templateBusy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/from_template",language:this._lang,template_id:e});this._templateGalleryOpen=!1,await this._loadData(),this._showToast(s("template_created",this._lang)),t?.entry_id&&this._showObject(t.entry_id)}catch{this._showToast(s("action_error",this._lang))}finally{this._templateBusy=!1}}_categoryName(e){let t=this._templateCategories[e];return t&&(t[`name_${this._lang}`]||t.name_en)||e}_renderTemplateGallery(){if(!this._templateGalleryOpen)return h;let e=this._lang,t=new Map;for(let i of this._templates)t.has(i.category)||t.set(i.category,[]),t.get(i.category).push(i);return o`
<div class="palette-backdrop" @click=${()=>{this._templateGalleryOpen=!1}}>
<div class="template-gallery" @click=${i=>i.stopPropagation()}>
<div class="template-gallery-head">
@@ -38,22 +38,27 @@ def find_fleet_entry(hass: HomeAssistant) -> ConfigEntry | None:
return None
async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
async def async_setup_battery_fleet(hass: HomeAssistant, language: str | None = None) -> dict[str, Any]:
"""Create (or return) the Battery Fleet object with type-parts + the task.
Idempotent: a second call reconciles the type-parts against the current
fleet (adds parts for newly-seen types) and returns the existing entry.
``language`` is the caller's UI language (same contract as the template
WS): the created object/task/part names and notes are localized through
the ``_T`` table, falling back to the server language, then English.
"""
from ..websocket.objects import async_create_object
from ..websocket.tasks_persist import async_persist_task
from .i18n import normalize_language
from .parts import normalize_part
lang = (language or normalize_language(hass))[:2].lower()
types = discover_battery_types(hass) # {TYPE: total_qty}
existing = find_fleet_entry(hass)
if existing is not None:
added = _reconcile_type_parts(hass, existing, types)
repaired = await _reconcile_fleet_task(hass, existing)
added = _reconcile_type_parts(hass, existing, types, lang)
repaired = await _reconcile_fleet_task(hass, existing, lang)
return {
"entry_id": existing.entry_id,
"created": False,
@@ -62,7 +67,9 @@ async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
"task_repaired": repaired,
}
entry_id = await async_create_object(hass, name="Battery Fleet")
from ..templates import localize_template_text
entry_id = await async_create_object(hass, name=localize_template_text("Battery Fleet", lang) or "Battery Fleet")
entry = hass.config_entries.async_get_entry(entry_id)
if entry is None: # pragma: no cover — just created above
raise HomeAssistantError("Battery Fleet object entry vanished after creation")
@@ -74,7 +81,7 @@ async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
new_data[CONF_OBJECT] = obj
parts: dict[str, dict[str, Any]] = {}
for btype, total_qty in types.items():
part = normalize_part(_type_part(btype, total_qty))
part = normalize_part(_type_part(btype, total_qty, lang))
parts[part["id"]] = part
new_data[CONF_PARTS] = parts
hass.config_entries.async_update_entry(entry, data=new_data)
@@ -88,19 +95,7 @@ async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
await store.async_save()
# The single aggregate task, triggered by the global low-count sensor.
obj_id = obj.get("id", "")
task = {
"id": uuid4().hex,
"object_id": obj_id,
"name": "Replace low batteries",
"type": "inspection",
"enabled": True,
TASK_FLAG: True,
"schedule": {"kind": "manual"},
"trigger_config": _fleet_trigger_config(),
"created_at": dt_util.now().date().isoformat(),
"notes": ("Aggregate battery check. The detail view lists which devices are low and which battery types to buy."),
}
task = _fleet_task(obj.get("id", ""), lang)
await async_persist_task(hass, entry, task)
return {
@@ -130,23 +125,48 @@ def _fleet_trigger_config() -> dict[str, Any]:
}
def _type_part(btype: str, total_qty: int) -> dict[str, Any]:
"""A spare-part definition for one battery type.
def _fleet_task(obj_id: str, lang: str) -> dict[str, Any]:
"""The single aggregate fleet task, with localized name + notes."""
from ..templates import localize_template_text
return {
"id": uuid4().hex,
"object_id": obj_id,
"name": localize_template_text("Replace low batteries", lang) or "Replace low batteries",
"type": "inspection",
"enabled": True,
TASK_FLAG: True,
"schedule": {"kind": "manual"},
"trigger_config": _fleet_trigger_config(),
"created_at": dt_util.now().date().isoformat(),
"notes": localize_template_text(
"Aggregate battery check. The detail view lists which devices are low and which battery types to buy.",
lang,
),
}
def _type_part(btype: str, total_qty: int, lang: str) -> dict[str, Any]:
"""A spare-part definition for one battery type (localized name/notes).
reorder_threshold defaults to keeping a spare set roughly the size of the
fleet's need for that type (min 2); restock is double that. auto_buy_task
stays off so setup never spawns extra buy-tasks the fleet task's detail
is the shopping surface; the user can enable auto-buy per type later.
"""
from ..templates import localize_template_text
threshold = max(2, total_qty)
name_tpl = localize_template_text("{type} battery", lang) or "{type} battery"
notes_tpl = localize_template_text("Typical service life ~{months} months.", lang) or "Typical service life ~{months} months."
return {
"id": f"batt_{btype.lower()}",
"name": f"{btype} battery",
"name": name_tpl.format(type=btype),
"unit": "pcs",
"reorder_threshold": threshold,
"restock_quantity": threshold * 2,
"auto_buy_task": False,
"notes": f"Typical service life ~{lifetime_months(btype)} months (editorial).",
"notes": notes_tpl.format(months=lifetime_months(btype)),
}
@@ -223,12 +243,12 @@ def fleet_task_trigger_ok(entry: ConfigEntry) -> bool:
return tc.get("type") == "threshold" and LOW_COUNT_ENTITY_ID in eids
async def _reconcile_fleet_task(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _reconcile_fleet_task(hass: HomeAssistant, entry: ConfigEntry, lang: str) -> bool:
"""Repair the fleet task if broken. Returns True when something was fixed.
* Trigger lost (issue #106) → restore the canonical threshold trigger,
keeping the user's name/type/translations untouched.
* Task deleted entirely recreate it fresh.
* Task deleted entirely recreate it fresh (localized).
"""
from ..websocket.tasks_persist import async_persist_task
@@ -249,23 +269,11 @@ async def _reconcile_fleet_task(hass: HomeAssistant, entry: ConfigEntry) -> bool
return True
obj = entry.data.get(CONF_OBJECT, {})
task = {
"id": uuid4().hex,
"object_id": obj.get("id", ""),
"name": "Replace low batteries",
"type": "inspection",
"enabled": True,
TASK_FLAG: True,
"schedule": {"kind": "manual"},
"trigger_config": _fleet_trigger_config(),
"created_at": dt_util.now().date().isoformat(),
"notes": ("Aggregate battery check. The detail view lists which devices are low and which battery types to buy."),
}
await async_persist_task(hass, entry, task)
await async_persist_task(hass, entry, _fleet_task(obj.get("id", ""), lang))
return True
def _reconcile_type_parts(hass: HomeAssistant, entry: ConfigEntry, types: dict[str, int]) -> int:
def _reconcile_type_parts(hass: HomeAssistant, entry: ConfigEntry, types: dict[str, int], lang: str) -> int:
"""Add parts for battery types newly seen since setup. Returns count added."""
from .parts import normalize_part
@@ -275,7 +283,7 @@ def _reconcile_type_parts(hass: HomeAssistant, entry: ConfigEntry, types: dict[s
for btype, total_qty in types.items():
pid = f"batt_{btype.lower()}"
if pid not in existing_ids:
parts[pid] = normalize_part(_type_part(btype, total_qty))
parts[pid] = normalize_part(_type_part(btype, total_qty, lang))
added += 1
if added:
new_data = dict(entry.data)
@@ -22,5 +22,5 @@
"requirements": [
"pypdf>=4.3.0"
],
"version": "2.38.1"
"version": "2.38.3"
}
@@ -4329,6 +4329,105 @@ _T: dict[str, dict[str, str]] = {
"hi": "CO2 बोतल बदलें",
"zh": "更换CO2气瓶",
},
# ── Battery Fleet seeding (helpers/battery_fleet_setup.py) ──────────────
# The object/task/part names + notes the one-click fleet setup creates.
# Object name mirrors the panel's battery_fleet_title locale values so the
# created object matches the detail section's heading.
"Battery Fleet": {
"de": "Batterie-Flotte",
"es": "Flota de baterías",
"fr": "Parc de piles",
"it": "Parco batterie",
"nl": "Batterijvloot",
"pt": "Frota de baterias",
"ru": "Парк батарей",
"uk": "Парк батарей",
"pl": "Flota baterii",
"cs": "Flotila baterií",
"sv": "Batteriflotta",
"da": "Batteriflåde",
"nb": "Batteriflåte",
"fi": "Paristokanta",
"ja": "電池フリート",
"hi": "बैटरी फ्लीट",
"zh": "电池群",
},
"Replace low batteries": {
"de": "Schwache Batterien ersetzen",
"es": "Reemplazar baterías bajas",
"fr": "Remplacer les piles faibles",
"it": "Sostituire le batterie scariche",
"nl": "Zwakke batterijen vervangen",
"pt": "Substituir baterias fracas",
"ru": "Заменить разряженные батареи",
"uk": "Замінити розряджені батареї",
"pl": "Wymień słabe baterie",
"cs": "Vyměnit slabé baterie",
"sv": "Byt svaga batterier",
"da": "Udskift svage batterier",
"nb": "Bytt svake batterier",
"fi": "Vaihda heikot paristot",
"ja": "残量低下の電池を交換",
"hi": "कम बैटरी बदलें",
"zh": "更换低电量电池",
},
"Aggregate battery check. The detail view lists which devices are low and which battery types to buy.": {
"de": "Gesammelte Batterie-Prüfung. Die Detailansicht zeigt, welche Geräte schwach sind und welche Batterietypen zu kaufen sind.",
"es": "Comprobación agregada de baterías. La vista de detalle muestra qué dispositivos están bajos y qué tipos de batería comprar.",
"fr": "Contrôle groupé des piles. La vue détaillée liste les appareils faibles et les types de piles à acheter.",
"it": "Controllo aggregato delle batterie. La vista di dettaglio elenca i dispositivi scarichi e i tipi di batteria da acquistare.",
"nl": "Gebundelde batterijcontrole. De detailweergave toont welke apparaten zwak zijn en welke batterijtypes te kopen zijn.",
"pt": "Verificação agregada de baterias. A vista de detalhe lista os dispositivos fracos e os tipos de bateria a comprar.",
"ru": "Сводная проверка батарей. В подробном виде показано, какие устройства разряжены и какие типы батарей купить.",
"uk": "Зведена перевірка батарей. У детальному перегляді показано, які пристрої розряджені та які типи батарей купити.",
"pl": "Zbiorcza kontrola baterii. Widok szczegółów pokazuje, które urządzenia są słabe i jakie typy baterii kupić.",
"cs": "Souhrnná kontrola baterií. Detailní pohled ukazuje, která zařízení jsou slabá a jaké typy baterií koupit.",
"sv": "Samlad batterikontroll. Detaljvyn visar vilka enheter som är svaga och vilka batterityper som ska köpas.",
"da": "Samlet batteritjek. Detaljevisningen viser, hvilke enheder der er svage, og hvilke batterityper der skal købes.",
"nb": "Samlet batterisjekk. Detaljvisningen viser hvilke enheter som er svake og hvilke batterityper som må kjøpes.",
"fi": "Koottu paristotarkistus. Yksityiskohtanäkymä näyttää, mitkä laitteet ovat heikkoja ja mitä paristotyyppejä ostaa.",
"ja": "電池のまとめて点検。詳細ビューには残量低下の機器と購入する電池の種類が表示されます。",
"hi": "समग्र बैटरी जाँच। विवरण दृश्य दिखाता है कि कौन से उपकरण कम हैं और कौन से बैटरी प्रकार खरीदने हैं।",
"zh": "电池汇总检查。详情视图列出哪些设备电量低以及需要购买哪些电池类型。",
},
"{type} battery": {
"de": "{type}-Batterie",
"es": "Batería {type}",
"fr": "Pile {type}",
"it": "Batteria {type}",
"nl": "{type}-batterij",
"pt": "Bateria {type}",
"ru": "Батарейка {type}",
"uk": "Батарейка {type}",
"pl": "Bateria {type}",
"cs": "Baterie {type}",
"sv": "{type}-batteri",
"da": "{type}-batteri",
"nb": "{type}-batteri",
"fi": "{type}-paristo",
"ja": "{type}電池",
"hi": "{type} बैटरी",
"zh": "{type}电池",
},
"Typical service life ~{months} months.": {
"de": "Typische Lebensdauer ~{months} Monate.",
"es": "Vida útil típica ~{months} meses.",
"fr": "Durée de vie typique ~{months} mois.",
"it": "Durata tipica ~{months} mesi.",
"nl": "Typische levensduur ~{months} maanden.",
"pt": "Vida útil típica ~{months} meses.",
"ru": "Типичный срок службы ~{months} мес.",
"uk": "Типовий термін служби ~{months} міс.",
"pl": "Typowa żywotność ~{months} mies.",
"cs": "Typická životnost ~{months} měsíců.",
"sv": "Typisk livslängd ~{months} månader.",
"da": "Typisk levetid ~{months} måneder.",
"nb": "Typisk levetid ~{months} måneder.",
"fi": "Tyypillinen käyttöikä ~{months} kk.",
"ja": "標準的な寿命 約{months}か月。",
"hi": "सामान्य आयु ~{months} महीने।",
"zh": "典型使用寿命约{months}个月。",
},
}
@@ -1605,7 +1605,7 @@
},
"interval_unit": {
"name": "間隔の単位",
"description": "間隔の単位: days、weeks、months、years。既定値: days。"
"description": "間隔の単位: days、weeks、months、years のいずれか。既定値: days。"
},
"due_date": {
"name": "期限日",
@@ -1657,7 +1657,7 @@
},
"interval_unit": {
"name": "間隔の単位",
"description": "days、weeks、months、years。"
"description": "days、weeks、months、years のいずれか。"
},
"due_date": {
"name": "期限日",
@@ -1681,7 +1681,7 @@
},
"priority": {
"name": "優先度",
"description": "low、normal、high。"
"description": "low、normal、high のいずれか。"
},
"labels": {
"name": "ラベル",

Some files were not shown because too many files have changed in this diff Show More