feat: French localization — community contribution by @OUARZA (#371)

The complete French dictionary contributed in #371 (1026 keys, zero empty
values, zero placeholder mismatches) lands as the second lazy locale on the
fr.ts + one static entry. The contributor's snapshot predated this week's
keys, so the 121 additions (device inbox #29, backdrop guard #39, junction
limits #331, lazy-editor toasts #353/#354, furniture #159) are translated
in this commit and flagged in the issue for the author's review; 21 stale
pre-#62 keys are dropped; key order follows en.json. The HA integration
translations file ships as contributed (full parity).

Wiring: loadFrench + __HOUSEPLAN_FR_RETRY_ASSET__ with the same 1/1
replacement guarantee as German; locale roles and localeRoots generalise to
(de|fr); the stale-entry fallback panel (#353) gains its French branch —
the r1 reviewer caught that this second hardcoded language list would have
silently degraded French to English on a cached entry. French profiles
(fr, fr-FR, fr-CA, fr-BE, fr-CH) select automatically.

Proofs: the registry-driven parity suite covers fr by construction
(1128/1128 keys); French analogues of both German-personal tests (product
glossary + non-translation scan with a reviewed equal-to-English
allow-list of 22 legitimate homographs); smoke_french_locale — fr-CA
profile commits French from the real bundle, one lazy chunk per page,
initial graph free of fr; smoke_entry_stale gains the French run; a
registry mutant drops the fr entry and is killed by the smoke. Initial
view: +0.5 KB (registry entry + fallback branch); the 23 KB dictionary is
lazy.

Issue: #371
User-Visible: yes
This commit is contained in:
Codex
2026-08-29 13:39:21 +03:00
parent fbf2c1bd41
commit 89fa8ca588
44 changed files with 1540 additions and 201 deletions
@@ -1,113 +1,125 @@
{
"schema": 1,
"fingerprint": "7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9",
"fingerprint": "0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-CUcd9nqP.js",
"houseplan-assets/houseplan-card-DVfaouS8.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 273864,
"initialViewGzipBytes": 273927,
"lazyFiles": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/de-B2D7PSGr.js",
"houseplan-assets/editor-D4UEfEiU.js",
"houseplan-assets/houseplan-editor-runtime-C74b7OEh.js",
"houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/de-BNdN-RRm.js",
"houseplan-assets/editor-D4sIJVSN.js",
"houseplan-assets/fr-Ct3lI4wy.js",
"houseplan-assets/houseplan-editor-runtime-CdrLJACD.js",
"houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js"
],
"lazyGzipBytes": 169052,
"lazyGzipBytes": 191420,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/editor-D4UEfEiU.js",
"houseplan-assets/houseplan-editor-runtime-C74b7OEh.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/editor-D4sIJVSN.js",
"houseplan-assets/houseplan-editor-runtime-CdrLJACD.js"
],
"lazyEditorGzipBytes": 139733,
"lazyEditorGzipBytes": 139724,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js"
],
"lazyOnboardingGzipBytes": 9454,
"lazyOnboardingGzipBytes": 9450,
"lazyLocaleFiles": [
"houseplan-assets/de-B2D7PSGr.js"
"houseplan-assets/de-BNdN-RRm.js",
"houseplan-assets/fr-Ct3lI4wy.js"
],
"lazyLocaleGzipBytes": 22822,
"lazyLocaleGzipBytes": 45200,
"files": [
{
"path": "houseplan-assets/backdrop-pick-D3CsLggT.js",
"sha256": "c573ef439ebba184301b9d3dea49842f5ef911979c0e68811a45da88f59981ca",
"path": "houseplan-assets/backdrop-pick-DIYZEnDX.js",
"sha256": "36d8b3a260416b62b4374b6919a0351609f305a2a520bdd66730d30b9771b06a",
"rawBytes": 6967,
"gzipBytes": 2957,
"gzipBytes": 2954,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-B2D7PSGr.js",
"sha256": "741393fb2edb0a52b46ce804688ccd0776b8701d0205fc1c0aa1e3eeaeab8d7e",
"path": "houseplan-assets/de-BNdN-RRm.js",
"sha256": "80b714e7674f479be1ab8f85ec17e5837c5fbb4b443782ceaf09d75dfb0aeff8",
"rawBytes": 74912,
"gzipBytes": 22822,
"gzipBytes": 22810,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-D4UEfEiU.js",
"sha256": "afe48ab979d2784a6fc91c3f4ccb5792b8f6f09766a7c58ac767c95e487bcbeb",
"path": "houseplan-assets/editor-D4sIJVSN.js",
"sha256": "4fae099689ad8b84e00f316674ef4ffa7453b05724c3cc5e4bfd0c93a393aad6",
"rawBytes": 3831,
"gzipBytes": 1584,
"gzipBytes": 1581,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-CUcd9nqP.js",
"sha256": "4a30220dcf8acf66ae48ce7a7010bbdaf5025f57553dc3216003a89ab087c248",
"rawBytes": 964549,
"gzipBytes": 273108,
"path": "houseplan-assets/fr-Ct3lI4wy.js",
"sha256": "27ce761a57309ee4a21cfed2e0c8dd4923268e397024b1e654313ae88b5a0c94",
"rawBytes": 77034,
"gzipBytes": 22390,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DVfaouS8.js",
"sha256": "358f2f5044dce46a9ba33638245d94f156b5d5a713149c0af18cd81bf75e5745",
"rawBytes": 964793,
"gzipBytes": 273134,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-B2D7PSGr.js",
"houseplan-assets/editor-D4UEfEiU.js",
"houseplan-assets/houseplan-editor-runtime-C74b7OEh.js",
"houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js"
"houseplan-assets/de-BNdN-RRm.js",
"houseplan-assets/editor-D4sIJVSN.js",
"houseplan-assets/fr-Ct3lI4wy.js",
"houseplan-assets/houseplan-editor-runtime-CdrLJACD.js",
"houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-C74b7OEh.js",
"sha256": "9c2b2608ca55e3e98c8b91f5b6ff772ea573ea08cf9e26220e8530078df4b98b",
"path": "houseplan-assets/houseplan-editor-runtime-CdrLJACD.js",
"sha256": "c2da6c50e1d8878574d7c5b9a6e46ee2d3db32dca6ae1c9a0505f873028dec93",
"rawBytes": 523581,
"gzipBytes": 135192,
"gzipBytes": 135189,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js",
"sha256": "235e274ca2c6b64e597310d2acdee957cd0f9478b30112b8c2f15731c887f2ae",
"path": "houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js",
"sha256": "044745db6ea2019526757e130a79a6dbb8003f434d42f99eedffc2a2dd0dcefa",
"rawBytes": 26195,
"gzipBytes": 6497,
"gzipBytes": 6496,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "de9507e4142ad25866ad053355bb39d517b6fd9d76c2da947baefc262b620212",
"rawBytes": 1089,
"gzipBytes": 756,
"sha256": "d36cadb50f439e2fe8cd08daf0357a0cc48dd9c2f221f081a7705b7bc6de4384",
"rawBytes": 1183,
"gzipBytes": 793,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-CUcd9nqP.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),u=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-DVfaouS8.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${u(h.slice(0,3))}
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${u(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-CUcd9nqP.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-D3CsLggT.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";class F{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:n,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-DVfaouS8.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-DIYZEnDX.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";class F{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:n,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
${s.map(s=>{return _`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";try{await import("./houseplan-assets/houseplan-card-CUcd9nqP.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";try{await import("./houseplan-assets/houseplan-card-DVfaouS8.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
@@ -0,0 +1,30 @@
{
"config": {
"step": {
"user": {
"title": "House Plan",
"data": {
"admin_only": "Seuls les administrateurs peuvent modifier le plan"
}
}
},
"abort": {
"single_instance_allowed": "Déjà configuré — une seule entrée est autorisée."
}
},
"options": {
"step": {
"init": {
"data": {
"admin_only": "Seuls les administrateurs peuvent modifier le plan"
}
}
}
},
"issues": {
"broken_plan": {
"title": "L’image du plan est introuvable",
"description": "Le fichier de plan `{file}` pour l’espace `{space}` est introuvable dans `config/houseplan/plans/`. Ouvrez les paramètres de l’espace dans la carte House Plan et téléversez de nouveau le plan."
}
}
}
+4 -1
View File
@@ -41,12 +41,15 @@ const run = async (locale, expected) => {
const en = await run('en-US');
const ru = await run('ru-RU');
const fr = await run('fr-FR');
const out = {
entrySurvivesMissingChunk: en.pageErrors === 0 && ru.pageErrors === 0,
entrySurvivesMissingChunk: en.pageErrors === 0 && ru.pageErrors === 0 && fr.pageErrors === 0,
englishMessageVisible: en.text.includes('House Plan was updated')
&& en.text.includes('reload the page'),
russianMessageVisible: ru.text.includes('House Plan обновился')
&& ru.text.includes('перезагрузите страницу'),
frenchMessageVisible: fr.text.includes('House Plan a été mis à jour')
&& fr.text.includes('recharger la page'),
};
checkAll(out);
check('smoke_entry_stale', Object.values(out).every(Boolean));
+39
View File
@@ -0,0 +1,39 @@
// #371: French is the second lazy locale — community-contributed dictionary
// (@OUARZA). This smoke pins the fr-specific wiring only: automatic selection
// from an fr-CA profile, one lazy chunk per page, and the initial graph free
// of French. The generic failure machinery (retry, fallback, render gate,
// failure toast) is locale-agnostic and stays proven by smoke_german_locale.
import { readFileSync } from 'node:fs';
import { launchColdView, checkAll, finish } from './serve.mjs';
const manifest = JSON.parse(readFileSync('dist/houseplan-assets.json', 'utf8'));
const frPath = manifest.files.map((f) => f.path).find((p) => /(?:^|\/)fr-[^/]+\.js$/.test(p));
if (!frPath) throw new Error('French locale chunk is absent from the bundle manifest');
const frName = frPath.split('/').at(-1);
const { page, browser } = await launchColdView();
const requested = [];
page.on('request', (r) => requested.push(new URL(r.url()).pathname));
const out = await page.evaluate(async (frChunk) => {
const card = window.__card;
const out = {};
const waitFor = async (predicate, timeout = 8000) => {
const started = Date.now();
while (!predicate() && Date.now() - started < timeout) {
await new Promise((resolve) => setTimeout(resolve, 40));
}
return !!predicate();
};
// профиль HA fr-CA → французский, без явного выбора
card.hass = { ...card.hass, locale: { ...card.hass.locale, language: 'fr-CA' } };
card.requestUpdate();
out.frenchCommits = await waitFor(() => card._t('btn.save') === 'Enregistrer');
out.frenchTitleGlossary = card._t('space.header') === 'Espace';
out.langAttrIsFr = await waitFor(() => card.getAttribute('lang') === 'fr');
return out;
}, frName);
out.frChunkRequestedOnce = requested.filter((p) => p.endsWith(`/${frName}`)).length === 1;
out.initialGraphHasNoFrench = !manifest.initialViewFiles.some((p) => /(?:^|\/)fr-[^/]+\.js$/.test(p))
&& manifest.lazyLocaleFiles.some((p) => /(?:^|\/)fr-[^/]+\.js$/.test(p));
checkAll(out);
await finish(browser, out);
+63 -51
View File
@@ -1,113 +1,125 @@
{
"schema": 1,
"fingerprint": "7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9",
"fingerprint": "0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-CUcd9nqP.js",
"houseplan-assets/houseplan-card-DVfaouS8.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 273864,
"initialViewGzipBytes": 273927,
"lazyFiles": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/de-B2D7PSGr.js",
"houseplan-assets/editor-D4UEfEiU.js",
"houseplan-assets/houseplan-editor-runtime-C74b7OEh.js",
"houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/de-BNdN-RRm.js",
"houseplan-assets/editor-D4sIJVSN.js",
"houseplan-assets/fr-Ct3lI4wy.js",
"houseplan-assets/houseplan-editor-runtime-CdrLJACD.js",
"houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js"
],
"lazyGzipBytes": 169052,
"lazyGzipBytes": 191420,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/editor-D4UEfEiU.js",
"houseplan-assets/houseplan-editor-runtime-C74b7OEh.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/editor-D4sIJVSN.js",
"houseplan-assets/houseplan-editor-runtime-CdrLJACD.js"
],
"lazyEditorGzipBytes": 139733,
"lazyEditorGzipBytes": 139724,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js"
],
"lazyOnboardingGzipBytes": 9454,
"lazyOnboardingGzipBytes": 9450,
"lazyLocaleFiles": [
"houseplan-assets/de-B2D7PSGr.js"
"houseplan-assets/de-BNdN-RRm.js",
"houseplan-assets/fr-Ct3lI4wy.js"
],
"lazyLocaleGzipBytes": 22822,
"lazyLocaleGzipBytes": 45200,
"files": [
{
"path": "houseplan-assets/backdrop-pick-D3CsLggT.js",
"sha256": "c573ef439ebba184301b9d3dea49842f5ef911979c0e68811a45da88f59981ca",
"path": "houseplan-assets/backdrop-pick-DIYZEnDX.js",
"sha256": "36d8b3a260416b62b4374b6919a0351609f305a2a520bdd66730d30b9771b06a",
"rawBytes": 6967,
"gzipBytes": 2957,
"gzipBytes": 2954,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-B2D7PSGr.js",
"sha256": "741393fb2edb0a52b46ce804688ccd0776b8701d0205fc1c0aa1e3eeaeab8d7e",
"path": "houseplan-assets/de-BNdN-RRm.js",
"sha256": "80b714e7674f479be1ab8f85ec17e5837c5fbb4b443782ceaf09d75dfb0aeff8",
"rawBytes": 74912,
"gzipBytes": 22822,
"gzipBytes": 22810,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-D4UEfEiU.js",
"sha256": "afe48ab979d2784a6fc91c3f4ccb5792b8f6f09766a7c58ac767c95e487bcbeb",
"path": "houseplan-assets/editor-D4sIJVSN.js",
"sha256": "4fae099689ad8b84e00f316674ef4ffa7453b05724c3cc5e4bfd0c93a393aad6",
"rawBytes": 3831,
"gzipBytes": 1584,
"gzipBytes": 1581,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-CUcd9nqP.js",
"sha256": "4a30220dcf8acf66ae48ce7a7010bbdaf5025f57553dc3216003a89ab087c248",
"rawBytes": 964549,
"gzipBytes": 273108,
"path": "houseplan-assets/fr-Ct3lI4wy.js",
"sha256": "27ce761a57309ee4a21cfed2e0c8dd4923268e397024b1e654313ae88b5a0c94",
"rawBytes": 77034,
"gzipBytes": 22390,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DVfaouS8.js",
"sha256": "358f2f5044dce46a9ba33638245d94f156b5d5a713149c0af18cd81bf75e5745",
"rawBytes": 964793,
"gzipBytes": 273134,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-B2D7PSGr.js",
"houseplan-assets/editor-D4UEfEiU.js",
"houseplan-assets/houseplan-editor-runtime-C74b7OEh.js",
"houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js"
"houseplan-assets/de-BNdN-RRm.js",
"houseplan-assets/editor-D4sIJVSN.js",
"houseplan-assets/fr-Ct3lI4wy.js",
"houseplan-assets/houseplan-editor-runtime-CdrLJACD.js",
"houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-C74b7OEh.js",
"sha256": "9c2b2608ca55e3e98c8b91f5b6ff772ea573ea08cf9e26220e8530078df4b98b",
"path": "houseplan-assets/houseplan-editor-runtime-CdrLJACD.js",
"sha256": "c2da6c50e1d8878574d7c5b9a6e46ee2d3db32dca6ae1c9a0505f873028dec93",
"rawBytes": 523581,
"gzipBytes": 135192,
"gzipBytes": 135189,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-szhIDzwu.js",
"sha256": "235e274ca2c6b64e597310d2acdee957cd0f9478b30112b8c2f15731c887f2ae",
"path": "houseplan-assets/houseplan-onboarding-runtime-l_2tbTu9.js",
"sha256": "044745db6ea2019526757e130a79a6dbb8003f434d42f99eedffc2a2dd0dcefa",
"rawBytes": 26195,
"gzipBytes": 6497,
"gzipBytes": 6496,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D3CsLggT.js",
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/backdrop-pick-DIYZEnDX.js",
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "de9507e4142ad25866ad053355bb39d517b6fd9d76c2da947baefc262b620212",
"rawBytes": 1089,
"gzipBytes": 756,
"sha256": "d36cadb50f439e2fe8cd08daf0357a0cc48dd9c2f221f081a7705b7bc6de4384",
"rawBytes": 1183,
"gzipBytes": 793,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-CUcd9nqP.js"
"houseplan-assets/houseplan-card-DVfaouS8.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-CUcd9nqP.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),u=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";import{c as e,l as o,t,cZ as s,A as a,c_ as i,c$ as l,d0 as n,E as r,b as c}from"./houseplan-card-DVfaouS8.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${u(h.slice(0,3))}
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${u(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-CUcd9nqP.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-D3CsLggT.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";class F{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:n,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";import{bT as t,bU as s,bV as e,bW as o,aq as a,bX as l,c0 as i,bY as h,bZ as c,b_ as n,b$ as r,A as p,b as _,cR as d,ap as u,ar as g,cS as m}from"./houseplan-card-DVfaouS8.js";import{i as b,c as $,e as v,r as f,a as y,b as w,s as D,t as S}from"./backdrop-pick-DIYZEnDX.js";const k=1e3,C=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},M="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";class F{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),c=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,n="none"===h.fill?{...c||e,a:0}:c;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:n,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:n,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:c,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await $(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await v(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return f(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??p}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return _`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return _`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return _`<div class="savedplans">
${s.map(s=>{return _`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="7676e1b8fe51c36cc0235725188621ac7fbfe932106930ff3475c38e48ea03f9";try{await import("./houseplan-assets/houseplan-card-CUcd9nqP.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0f7edde68a64b82e24b51acbb2ba3050dbe2684c43fd3cb3428ee481b9de3d0f";try{await import("./houseplan-assets/houseplan-card-DVfaouS8.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
+6
View File
@@ -6,6 +6,12 @@
top framing strip together with the header, while preserving its side and
bottom spacing ([#372](https://github.com/Matysh/houseplan-card/issues/372)).
- House Plan now speaks French: a complete community translation contributed
by @OUARZA, loaded lazily like German so the ordinary View bundle stays the
same size. `Français` can be selected explicitly and is chosen
automatically for French Home Assistant profiles
([#371](https://github.com/Matysh/houseplan-card/issues/371)).
- Breaking for external writers (scripts, automations, custom integrations
that write plans directly): `houseplan/config/set` and `houseplan/layout/set`
over a non-empty store now require `expected_rev` and answer `conflict`
+5
View File
@@ -12,6 +12,11 @@
заголовком пустую полосу кадрирования сверху, сохраняя боковые и нижнее поля
([#372](https://github.com/Matysh/houseplan-card/issues/372)).
- House Plan заговорил по-французски: полный перевод сообщества от @OUARZA,
загружается лениво, как немецкий, — обычный View-бандл не вырос. `Français`
выбирается явно и включается автоматически для французских профилей
Home Assistant ([#371](https://github.com/Matysh/houseplan-card/issues/371)).
- Ломающее для внешних клиентов (скрипты, автоматизации, кастомные
интеграции, пишущие планы напрямую): `houseplan/config/set` и
`houseplan/layout/set` поверх непустого хранилища теперь требуют
+3 -1
View File
@@ -146,7 +146,9 @@ title: House plan
The legacy card-level `tap_action` is ignored. Each marker owns its action.
`auto` recognizes the primary Home Assistant locale: `de`, `de-DE`, `de-AT`
and `de-CH` use German. German is downloaded once on first use and shared by
and `de-CH` use German, while `fr`, `fr-FR`, `fr-CA`, `fr-BE` and `fr-CH` use
French (a community translation — thank you, @OUARZA). Lazy dictionaries
(German, French) are downloaded once on first use and shared by
all House Plan cards on the page. Until it is ready, a neutral busy surface is
shown instead of briefly flashing English; if both bounded download attempts
fail, the card becomes usable in English and says so with a "Could not load
+4 -2
View File
@@ -151,8 +151,10 @@ title: План дома
Карточечный `tap_action` из старых конфигураций игнорируется. Действие задаётся отдельно для каждого устройства.
`auto` учитывает основной язык профиля Home Assistant: `de`, `de-DE`,
`de-AT` и `de-CH` включают немецкий интерфейс. Немецкий словарь один раз
загружается при первом использовании и разделяется всеми карточками House Plan
`de-AT` и `de-CH` включают немецкий интерфейс, а `fr`, `fr-FR`, `fr-CA`,
`fr-BE` и `fr-CH` — французский (перевод сообщества, спасибо @OUARZA).
Ленивые словари (немецкий, французский) загружаются один раз
при первом использовании и разделяются всеми карточками House Plan
на странице. До готовности показывается нейтральный индикатор без краткой
вспышки английского текста; если обе ограниченные попытки загрузки не удались,
карточка остаётся работоспособной на английском и сообщает об этом тостом
Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+21 -21
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"sourceFingerprint": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "7078120b4fe7469f523cd7835b31fa646caed81fe1bc0f900690502898f1bea2"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "78d328498bd17a9ee1ec44b389d244b61fca9ac52c9aad6631aa85c53233fec0"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,8 +26,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "f234e55b02d2558c58935b3bc6340615fb343d3cf92b7d3a98303b1a2f4f346d"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
},
"space-create": {
"file": "03-space-create.png",
@@ -37,8 +37,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "a86d4211af24923d5048e9094d0d35f9b28129394d00cd72582a8f8c2dd7ff75"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "c7effd4ccd19bcaff9138458583368aaab488dcb6fc7ea5ab766383cb5377ac4"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "af1194f073f00b03af02b949a072cffb21bfd29995aa65a067eaaaec74ef8490"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "50ac3eeb9629e60b5e7eff65e40c75016f564d85fddf6d010908919e0e9ad18c"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "8affadfce926fe2f6cf344c33f611c44fcbc79c1969f69c3a6935ff2e99c36c3"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "7151c96dc048381f7a7070ecb85a82a890b2f4c703628f14fa472eb8dacbf3dc"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "0d427b615afbc54e357ea7eb931aa86e4d8cac6c63c294306b9b959d619936ad"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "ed041f571b0c34f1e06b13419ea08d90ebb7c26950f631d46f6cebb9a29f5c20"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "66d8b0a509909822ff6b891a483bad9d2c72b20f08e287355883aa5205c6cee0"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,8 +114,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "599840f21a79d08e8a205c972944079c00d1e09c9e9ceab4bcaecc3b40a0bbfb",
"imageSha256": "f4d3bda00a2fce54f408b1f20e03b5c590ba4b168fd214a28aafb1692b1f3124"
"sourceSha256": "35879df7ce2fb33d55eca90333259c43ce9db8b35379c1dab6bb8b09d7421d08",
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
}
}
}
+18 -4
View File
@@ -7,6 +7,7 @@ const BUILD_FINGERPRINT_TOKEN = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
const EDITOR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_EDITOR_RETRY_ASSET__';
const ONBOARDING_RETRY_ASSET_TOKEN = '__HOUSEPLAN_ONBOARDING_RETRY_ASSET__';
const DE_RETRY_ASSET_TOKEN = '__HOUSEPLAN_DE_RETRY_ASSET__';
const FR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FR_RETRY_ASSET__';
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
@@ -29,7 +30,7 @@ export function buildBundleManifest(bundle, fingerprint) {
.map((chunk) => {
const contents = Buffer.from(chunk.code, 'utf8');
const modules = Object.keys(chunk.modules || {}).map((id) => id.replaceAll('\\', '/'));
const role = modules.some((id) => id.endsWith('/src/i18n/de.ts'))
const role = modules.some((id) => id.endsWith('/src/i18n/de.ts') || id.endsWith('/src/i18n/fr.ts'))
? 'locale'
: modules.some((id) => id.endsWith('/src/houseplan-onboarding-runtime.ts'))
? 'onboarding'
@@ -59,7 +60,7 @@ export function buildBundleManifest(bundle, fingerprint) {
for (const path of reachable(root, byPath, 'imports')) if (!initial.has(path)) lazy.add(path);
}
const localeRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'locale'
|| /(?:^|\/)de-[^/]+\.js$/.test(path));
|| /(?:^|\/)(?:de|fr)-[^/]+\.js$/.test(path));
const onboardingRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'onboarding'
|| path.includes('houseplan-onboarding-runtime-'));
const editorRoots = dynamicRoots.filter((path) => (byPath.get(path)?._role === 'editor'
@@ -140,12 +141,16 @@ export function editorRuntimeRetryUrlPlugin() {
.some((id) => id.replaceAll('\\', '/').endsWith('/src/houseplan-onboarding-runtime.ts')));
const german = chunks.find((chunk) => Object.keys(chunk.modules)
.some((id) => id.replaceAll('\\', '/').endsWith('/src/i18n/de.ts')));
const french = chunks.find((chunk) => Object.keys(chunk.modules)
.some((id) => id.replaceAll('\\', '/').endsWith('/src/i18n/fr.ts')));
if (!editor) throw new Error('editor runtime chunk was not emitted');
if (!onboarding) throw new Error('onboarding runtime chunk was not emitted');
if (!german) throw new Error('German locale chunk was not emitted');
if (!french) throw new Error('French locale chunk was not emitted');
let editorReplacements = 0;
let onboardingReplacements = 0;
let germanReplacements = 0;
let frenchReplacements = 0;
for (const chunk of chunks) {
if (chunk.code.includes(EDITOR_RETRY_ASSET_TOKEN)) {
let asset = posix.relative(posix.dirname(chunk.fileName), editor.fileName);
@@ -165,10 +170,17 @@ export function editorRuntimeRetryUrlPlugin() {
germanReplacements += chunk.code.split(DE_RETRY_ASSET_TOKEN).length - 1;
chunk.code = chunk.code.replaceAll(DE_RETRY_ASSET_TOKEN, asset);
}
if (chunk.code.includes(FR_RETRY_ASSET_TOKEN)) {
let asset = posix.relative(posix.dirname(chunk.fileName), french.fileName);
if (!asset.startsWith('.')) asset = `./${asset}`;
frenchReplacements += chunk.code.split(FR_RETRY_ASSET_TOKEN).length - 1;
chunk.code = chunk.code.replaceAll(FR_RETRY_ASSET_TOKEN, asset);
}
}
if (editorReplacements !== 1 || onboardingReplacements !== 1 || germanReplacements !== 1) {
if (editorReplacements !== 1 || onboardingReplacements !== 1
|| germanReplacements !== 1 || frenchReplacements !== 1) {
throw new Error('lazy retry URL placeholder counts are '
+ `${editorReplacements}/${onboardingReplacements}/${germanReplacements}, expected 1/1/1`);
+ `${editorReplacements}/${onboardingReplacements}/${germanReplacements}/${frenchReplacements}, expected 1/1/1/1`);
}
},
};
@@ -209,6 +221,8 @@ export function entryFallbackPlugin() {
+ '?"House Plan обновился — перезагрузите страницу (Ctrl+F5)."'
+ ':l.startsWith("de")'
+ '?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5)."'
+ ':l.startsWith("fr")'
+ '?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5)."'
+ ':"House Plan was updated — please reload the page (Ctrl+F5).";'
+ 'customElements.define("houseplan-card",class extends HTMLElement{'
+ 'setConfig(){}getCardSize(){return 1}connectedCallback(){'
+11
View File
@@ -680,6 +680,17 @@ const MUTANT_DEFINITIONS = [
replace: ' this._languageFailureUnsub = undefined;',
}],
},
{
id: 'french-locale-entry-removed',
guard: 'node demo/smoke_french_locale.mjs',
because: 'dropping the fr registry entry silently reverts French profiles to English — the '
+ 'community contribution must stay wired (#371)',
patches: [{
file: 'src/i18n/registry.ts',
find: " { code: 'fr', nativeLabel: 'Français', loadDictionary: loadFrench },\n",
replace: '',
}],
},
{
id: 'opening-light-quantum-identity',
guard: 'node --test --test-name-pattern="#366" test/logic.test.mjs',
+1 -1
View File
@@ -39,7 +39,7 @@ export const SMOKE_LINKS = [
'LanguageRuntime', 'LanguageRuntimeContract', 'languageRenderGate',
'dictionaryFor', 'ensureLanguage',
],
smokes: ['smoke_german_locale.mjs'],
smokes: ['smoke_german_locale.mjs', 'smoke_french_locale.mjs'],
because: 'the production-bundle scenario observes the lazy German chunk through network '
+ 'requests and four root render gates: neutral cold frame, atomic German commit, page-cache '
+ 'reuse and the bounded retry-to-English path cannot be proven from pure registry tests (#348)',
+1130
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
import dictionary from './fr.json' with { type: 'json' };
/** Must match the entry bundle before a lazy locale can be committed. */
export const fingerprint = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
export { dictionary };
+9
View File
@@ -9,6 +9,7 @@ import {
const BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
const GERMAN_RETRY_ASSET = '__HOUSEPLAN_DE_RETRY_ASSET__';
const FRENCH_RETRY_ASSET = '__HOUSEPLAN_FR_RETRY_ASSET__';
export interface LanguageEntry {
code: string;
@@ -24,6 +25,13 @@ async function loadGerman(attempt: 0 | 1): Promise<LazyLanguageModule> {
return { dictionary: module.dictionary, fingerprint: module.fingerprint };
}
async function loadFrench(attempt: 0 | 1): Promise<LazyLanguageModule> {
const module = attempt === 0
? await import('./fr')
: await import(/* @vite-ignore */ new URL(`${FRENCH_RETRY_ASSET}?retry`, import.meta.url).href);
return { dictionary: module.dictionary, fingerprint: module.fingerprint };
}
/**
* The single runtime registry of shipped UI languages.
*
@@ -34,6 +42,7 @@ export const LANGUAGE_REGISTRY = [
{ code: 'en', nativeLabel: 'English', dictionary: en },
{ code: 'ru', nativeLabel: 'Русский', dictionary: ru },
{ code: 'de', nativeLabel: 'Deutsch', loadDictionary: loadGerman },
{ code: 'fr', nativeLabel: 'Français', loadDictionary: loadFrench },
] as const satisfies readonly LanguageEntry[];
export type Lang = (typeof LANGUAGE_REGISTRY)[number]['code'];
+8 -2
View File
@@ -100,7 +100,8 @@ test('retry URL points at the content-hashed runtime chunk after naming', () =>
type: 'chunk', fileName: 'houseplan-assets/houseplan-card.js',
code: 'new URL("__HOUSEPLAN_EDITOR_RETRY_ASSET__", import.meta.url);'
+ 'new URL("__HOUSEPLAN_ONBOARDING_RETRY_ASSET__", import.meta.url);'
+ 'new URL("__HOUSEPLAN_DE_RETRY_ASSET__", import.meta.url)', modules: {},
+ 'new URL("__HOUSEPLAN_DE_RETRY_ASSET__", import.meta.url);'
+ 'new URL("__HOUSEPLAN_FR_RETRY_ASSET__", import.meta.url)', modules: {},
},
'houseplan-assets/houseplan-editor-runtime-HASH.js': {
type: 'chunk', fileName: 'houseplan-assets/houseplan-editor-runtime-HASH.js', code: '',
@@ -114,13 +115,18 @@ test('retry URL points at the content-hashed runtime chunk after naming', () =>
type: 'chunk', fileName: 'houseplan-assets/de-HASH.js', code: '',
modules: { '/repo/src/i18n/de.ts': {} },
},
'houseplan-assets/fr-HASH.js': {
type: 'chunk', fileName: 'houseplan-assets/fr-HASH.js', code: '',
modules: { '/repo/src/i18n/fr.ts': {} },
},
};
plugin.generateBundle({}, bundle);
assert.equal(
bundle['houseplan-assets/houseplan-card.js'].code,
'new URL("./houseplan-editor-runtime-HASH.js", import.meta.url);'
+ 'new URL("./houseplan-onboarding-runtime-HASH.js", import.meta.url);'
+ 'new URL("./de-HASH.js", import.meta.url)',
+ 'new URL("./de-HASH.js", import.meta.url);'
+ 'new URL("./fr-HASH.js", import.meta.url)',
);
});
+51 -1
View File
@@ -21,6 +21,7 @@ const dictionaries = new Map(LANGUAGE_REGISTRY.map(({ code }) => [code, dictiona
const en = dictionaries.get('en');
const ru = dictionaries.get('ru');
const de = dictionaries.get('de');
const fr = dictionaries.get('fr');
const cardSource = readHouseplanProductionSource();
test('i18n: registry codes and English fallback are valid', () => {
@@ -133,7 +134,7 @@ test('i18n: placeholders match between languages', () => {
});
test('i18n: resolver supports exact, primary, explicit and fallback paths', () => {
const supported = ['en', 'ru', 'de', 'pt-BR'];
const supported = ['en', 'ru', 'de', 'fr', 'pt-BR'];
const cases = [
{ explicit: 'RU', ha: 'en-US', expected: 'ru' },
{ explicit: 'pt_BR', ha: 'ru-RU', expected: 'pt-BR' },
@@ -145,6 +146,10 @@ test('i18n: resolver supports exact, primary, explicit and fallback paths', () =
{ explicit: undefined, ha: 'de-DE', expected: 'de' },
{ explicit: undefined, ha: 'de-AT', expected: 'de' },
{ explicit: undefined, ha: 'de-CH', expected: 'de' },
{ explicit: undefined, ha: 'fr-FR', expected: 'fr' },
{ explicit: undefined, ha: 'fr-CA', expected: 'fr' },
{ explicit: undefined, ha: 'fr_BE', expected: 'fr' },
{ explicit: undefined, ha: 'fr-CH', expected: 'fr' },
];
for (const { explicit, ha, expected } of cases) {
assert.equal(resolveLanguageCode(explicit, ha, supported, 'en'), expected);
@@ -156,6 +161,7 @@ test('i18n: langOf wires card config and both HA locale shapes to the registry',
assert.equal(langOf({ locale: { language: 'en-GB' }, language: 'ru' }, 'RU'), 'ru');
assert.equal(langOf({ language: 'ru-RU' }, 'unknown'), 'ru');
assert.equal(langOf({ locale: { language: 'de-DE' } }, 'unknown'), 'de');
assert.equal(langOf({ locale: { language: 'fr-CA' } }, 'unknown'), 'fr');
});
test('i18n: editor options follow registry order and preserve unknown raw values', () => {
@@ -368,3 +374,47 @@ test('i18n: German values equal to English are explicitly reviewed', () => {
const equalKeys = Object.keys(en).filter((key) => en[key] === de[key]);
assert.deepEqual(new Set(equalKeys), allowed);
});
test('i18n: French catalog keeps the product glossary and has no translation sentinels (#371)', () => {
assert.equal(fr['btn.save'], 'Enregistrer');
assert.equal(fr['btn.cancel'], 'Annuler');
assert.equal(fr['btn.delete'], 'Supprimer');
assert.equal(fr['space.header'], 'Espace');
assert.equal(fr['opening.passage'], 'Passage ouvert');
for (const [key, value] of Object.entries(fr)) {
assert.doesNotMatch(value, /ZXQPH|QXZ|⟦HP/u, `fr:${key} contains a translator sentinel`);
assert.doesNotMatch(value, /[А-Яа-яЁё]/u, `fr:${key} contains Cyrillic text`);
}
});
test('i18n: French values equal to English are explicitly reviewed (#371)', () => {
// Every entry here is a legitimate French/English homograph or a unit,
// template or brand token — reviewed with the contributed dictionary.
const allowed = new Set([
'color_picker.saturation',
'confirm.delete_partition_openings_item',
'decor.rect',
'err.code',
'furn.cat_bidet',
'gs.about_version',
'gs.north_letter',
'gs.unit_m',
'marker.desc_label',
'marker.preview.multiple_sources',
'marker.value_badge_attr_current_position',
'marker.value_badge_attr_volume_level',
'marker.value_badge_position',
'opening.type_label',
'rules.icon_ph',
'run.script',
'space.plan_alt',
'vac.cap_position',
'vac.diag_position',
'vac.diag_source',
'vac.documentation',
'wallthick.unit_cm',
]);
const equalKeys = Object.keys(en).filter((key) => en[key] === fr[key]);
assert.deepEqual(new Set(equalKeys), allowed);
});