From 056e9e0d744319ef60c10687563e4981a4912490 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 12 Nov 2017 20:48:42 -0500 Subject: [PATCH] Translations for core states (#575) * Fix deeper nested translations build * Make fallback to message optional * Use translated state names * Remove unused switch cases * Use src en.json as fallback instead of downloaded * Use separate translations for badge states * Eliminate unnecessary StatesMixin * Remove now unused localize fallback parameter * Fix capitalization to match material guidelines * Move media player text generation back to model * Make localize args object * Change Mixin to use computed function * Revert to normal args spread for haLocalize * Rename to computeHaLocalize * Allow state to default for badge and media player * Denormalize en.json with Lokalise placeholders * Fix cleanups missed after master merge * Split zwave query stage states to separate keys * Throw error to fail gulp build * Fix zwave template and regression on general state --- gulp/tasks/translations.js | 68 +++++- src/cards/ha-media_player-card.html | 13 +- .../entity/ha-state-label-badge.html | 44 ++-- src/components/ha-sidebar.html | 10 +- src/state-summary/state-card-display.html | 56 ++++- .../state-card-media_player.html | 12 +- src/translations/en.json | 204 ++++++++++++++++++ src/util/hass-mixins.html | 13 +- src/util/hass-util.html | 71 ------ src/util/media-player-model.html | 7 +- 10 files changed, 370 insertions(+), 128 deletions(-) diff --git a/gulp/tasks/translations.js b/gulp/tasks/translations.js index 246e01e4a5..d90e309c09 100755 --- a/gulp/tasks/translations.js +++ b/gulp/tasks/translations.js @@ -14,10 +14,10 @@ const outDir = 'build-translations'; const tasks = []; function recursiveFlatten(prefix, data) { - var output = {}; + let output = {}; Object.keys(data).forEach(function (key) { if (typeof (data[key]) === 'object') { - output = Object.assign({}, output, recursiveFlatten(key + '.', data[key])); + output = Object.assign({}, output, recursiveFlatten(prefix + key + '.', data[key])); } else { output[prefix + key] = data[key]; } @@ -29,16 +29,68 @@ function flatten(data) { return recursiveFlatten('', data); } -let taskName = 'build-merged-translations'; +/** + * Replace Lokalise key placeholders with their actual values. + * + * We duplicate the behavior of Lokalise here so that placeholders can + * be included in src/translations/en.json, but still be usable while + * developing locally. + * + * @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing + */ +const re_key_reference = /\[%key:([^%]+)%\]/; +function lokalise_transform (data, original) { + const output = {}; + Object.entries(data).forEach(([key, value]) => { + if (value instanceof Object) { + output[key] = lokalise_transform(value, original); + } else { + output[key] = value.replace(re_key_reference, (match, key) => { + const replace = key.split('::').reduce((tr, k) => tr[k], original); + if (typeof replace !== 'string') { + throw Error(`Invalid key placeholder ${key} in src/translations/en.json`); + } + return replace; + }); + } + }); + return output; +} + +/** + * This task will build a master translation file, to be used as the base for + * all languages. This starts with src/translations/en.json, and replaces all + * Lokalise key placeholders with their target values. Under normal circumstances, + * this will be the same as translations/en.json However, we build it here to + * facilitate both making changes in development mode, and to ensure that the + * project is buildable immediately after merging new translation keys, since + * the Lokalise update to translations/en.json will not happen immediately. + */ +let taskName = 'build-master-translation'; gulp.task(taskName, function () { + return gulp.src('src/translations/en.json') + .pipe(transform(function(data, file) { + return lokalise_transform(data, data); + })) + .pipe(rename('translation-master.json')) + .pipe(gulp.dest(outDir)); +}); +tasks.push(taskName); + +taskName = 'build-merged-translations'; +gulp.task(taskName, ['build-master-translation'], function () { return gulp.src(inDir + '/*.json') - .pipe(foreach(function (stream, file) { - // For each language generate a merged json file. It begins with en.json as - // a failsafe for untranslated strings, and merges all parent tags into one - // file for each specific subtag + .pipe(foreach(function(stream, file) { + // For each language generate a merged json file. It begins with the master + // translation as a failsafe for untranslated strings, and merges all parent + // tags into one file for each specific subtag + // + // TODO: This is a naive interpretation of BCP47 that should be improved. + // Will be OK for now as long as we don't have anything more complicated + // than a base translation + region. const tr = path.basename(file.history[0], '.json'); const subtags = tr.split('-'); - const src = [inDir + '/en.json']; // Start with en as a fallback for missing translations + const src = [outDir + '/translation-master.json']; for (let i = 1; i <= subtags.length; i++) { const lang = subtags.slice(0, i).join('-'); src.push(inDir + '/' + lang + '.json'); diff --git a/src/cards/ha-media_player-card.html b/src/cards/ha-media_player-card.html index 50d1c90df7..81490cdd5e 100644 --- a/src/cards/ha-media_player-card.html +++ b/src/cards/ha-media_player-card.html @@ -153,8 +153,8 @@
[[computeStateName(stateObj)]] -
[[playerObj.primaryText]]
- [[playerObj.secondaryText]]
+
[[computePrimaryText(haLocalize, playerObj)]]
+ [[playerObj.secondaryTitle]]
@@ -206,7 +206,8 @@ diff --git a/src/translations/en.json b/src/translations/en.json index 10b59b722a..31194ce5c3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -8,5 +8,209 @@ "log_out": "Log out", "mailbox": "Mailbox", "shopping_list": "Shopping list" + }, + "state": { + "default": { + "off": "Off", + "on": "On", + "unknown": "Unknown", + "unavailable": "Unavailable" + }, + "alarm_control_panel": { + "armed": "Armed", + "disarmed": "Disarmed", + "armed_home": "Armed home", + "armed_away": "Armed away", + "armed_night": "Armed night", + "pending": "Pending", + "arming": "Arming", + "disarming": "Disarming", + "triggered": "Triggered" + }, + "automation": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "binary_sensor": { + "default": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "moisture": { + "off": "Dry", + "on": "Wet" + }, + "gas": { + "off": "Clear", + "on": "Detected" + }, + "motion": { + "off": "[%key:state::binary_sensor::gas::off%]", + "on": "[%key:state::binary_sensor::gas::on%]" + }, + "occupancy": { + "off": "[%key:state::binary_sensor::gas::off%]", + "on": "[%key:state::binary_sensor::gas::on%]" + }, + "smoke": { + "off": "[%key:state::binary_sensor::gas::off%]", + "on": "[%key:state::binary_sensor::gas::on%]" + }, + "sound": { + "off": "[%key:state::binary_sensor::gas::off%]", + "on": "[%key:state::binary_sensor::gas::on%]" + }, + "vibration": { + "off": "[%key:state::binary_sensor::gas::off%]", + "on": "[%key:state::binary_sensor::gas::on%]" + }, + "opening": { + "off": "Closed", + "on": "Open" + }, + "safety": { + "off": "Safe", + "on": "Unsafe" + } + }, + "calendar": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "camera": { + "recording": "Recording", + "streaming": "Streaming", + "idle": "Idle" + }, + "climate": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]", + "heat": "Heat", + "cool": "Cool", + "idle": "Idle", + "auto": "Auto", + "dry": "Dry", + "fan_only": "Fan only", + "eco": "Eco", + "electric": "Electric", + "performance": "Performance", + "high_demand": "High demand", + "heat_pump": "Heat pump", + "gas": "Gas" + }, + "configurator": { + "configure": "Configure", + "configured": "Configured" + }, + "cover": { + "open": "Open", + "opening": "Opening", + "closed": "Closed", + "closing": "Closing", + "stopped": "Stopped" + }, + "device_tracker": { + "home": "Home", + "not_home": "Away" + }, + "fan": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "group": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]", + "home": "[%key:state::device_tracker::home%]", + "not_home": "[%key:state::device_tracker::not_home%]", + "open": "[%key:state::cover::open%]", + "opening": "[%key:state::cover::opening%]", + "closed": "[%key:state::cover::closed%]", + "closing": "[%key:state::cover::closing%]", + "stopped": "[%key:state::cover::stopped%]", + "locked": "[%key:state::lock::locked%]", + "unlocked": "[%key:state::lock::unlocked%]", + "ok": "[%key:state::plant::ok%]", + "problem": "[%key:state::plant::problem%]" + }, + "input_boolean": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "light": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "lock": { + "locked": "Locked", + "unlocked": "Unlocked" + }, + "media_player": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]", + "playing": "Playing", + "paused": "Paused", + "idle": "Idle", + "standby": "Standby" + }, + "plant": { + "ok": "OK", + "problem": "Problem" + }, + "remote": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "scene": { + "scening": "Scening" + }, + "script": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "sensor": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "sun": { + "above_horizon": "Above horizon", + "below_horizon": "Below horizon" + }, + "switch": { + "off": "[%key:state::default::off%]", + "on": "[%key:state::default::on%]" + }, + "zwave": { + "default": { + "initializing": "Initializing", + "dead": "Dead", + "sleeping": "Sleeping", + "ready": "Ready" + }, + "query_stage": { + "initializing": "[%key:state::zwave::default::initializing%] ({query_stage})", + "dead": "[%key:state::zwave::default::dead%] ({query_stage})" + } + } + }, + "state_badge": { + "default": { + "unknown": "Unk", + "unavailable": "Unavai" + }, + "alarm_control_panel": { + "armed": "Armed", + "disarmed": "Disarm", + "armed_home": "Armed", + "armed_away": "Armed", + "armed_night": "Armed", + "pending": "Pend", + "arming": "Arming", + "disarming": "Disarm", + "triggered": "Trig" + }, + "device_tracker": { + "home": "[%key:state::device_tracker::home%]", + "not_home": "[%key:state::device_tracker::not_home%]" + } } } diff --git a/src/util/hass-mixins.html b/src/util/hass-mixins.html index 024ee7c870..219db5fc35 100644 --- a/src/util/hass-mixins.html +++ b/src/util/hass-mixins.html @@ -82,9 +82,7 @@ window.hassMixins.LocalizeMixin = Polymer.dedupingMixin(superClass => class extends Polymer.mixinBehaviors([Polymer.AppLocalizeBehavior], superClass) { static get properties() { return { - hass: { - type: Object, - }, + hass: Object, language: { type: String, computed: 'computeLanguage(hass)', @@ -93,6 +91,10 @@ window.hassMixins.LocalizeMixin = Polymer.dedupingMixin(superClass => type: Object, computed: 'computeResources(hass)', }, + haLocalize: { + type: Function, + computed: 'computeHaLocalize(localize)', + }, }; } @@ -104,9 +106,8 @@ window.hassMixins.LocalizeMixin = Polymer.dedupingMixin(superClass => return hass && hass.resources; } - localize(namespace, message, ...args) { - // Return the input message if no translation is found - return super.localize(namespace + '.' + message, ...args) || message; + computeHaLocalize(localize) { + return (namespace, message, ...args) => localize(namespace + '.' + message, ...args); } }); diff --git a/src/util/hass-util.html b/src/util/hass-util.html index 8a428d6ee5..a1b9be3efe 100644 --- a/src/util/hass-util.html +++ b/src/util/hass-util.html @@ -471,77 +471,6 @@ window.hassUtil.sortByName = function (entityA, entityB) { return 0; }; -window.hassUtil.computeStateState = function (stateObj) { - if (!stateObj._stateDisplay) { - stateObj._stateDisplay = stateObj.state.replace(/_/g, ' '); - const domain = window.hassUtil.computeDomain(stateObj); - - if (stateObj.attributes.unit_of_measurement) { - stateObj._stateDisplay += ' ' + stateObj.attributes.unit_of_measurement; - } - if (domain === 'binary_sensor') { - switch (stateObj.attributes.device_class) { - case 'moisture': - stateObj._stateDisplay = (stateObj._stateDisplay === 'off') ? 'dry' : 'wet'; - break; - case 'gas': - case 'motion': - case 'occupancy': - case 'smoke': - case 'sound': - case 'vibration': - stateObj._stateDisplay = (stateObj._stateDisplay === 'off') ? 'clear' : 'detected'; - break; - case 'opening': - stateObj._stateDisplay = (stateObj._stateDisplay === 'off') ? 'closed' : 'open'; - break; - case 'safety': - stateObj._stateDisplay = (stateObj._stateDisplay === 'off') ? 'safe' : 'unsafe'; - break; - case 'cold': - case 'connectivity': - case 'heat': - case 'light': - case 'moving': - case 'power': - case 'plug': - default: - } - } else if (domain === 'input_datetime') { - let date; - if (!stateObj.attributes.has_time) { - date = new Date( - stateObj.attributes.year, - stateObj.attributes.month - 1, - stateObj.attributes.day - ); - stateObj._stateDisplay = window.hassUtil.formatDate(date); - } else if (!stateObj.attributes.has_date) { - date = new Date( - 1970, 0, 1, - stateObj.attributes.hour, - stateObj.attributes.minute - ); - stateObj._stateDisplay = window.hassUtil.formatTime(date); - } else { - date = new Date( - stateObj.attributes.year, stateObj.attributes.month - 1, - stateObj.attributes.day, stateObj.attributes.hour, - stateObj.attributes.minute - ); - stateObj._stateDisplay = window.hassUtil.formatDateTime(date); - } - } else if (domain === 'zwave') { - if (['initializing', 'dead'].includes(stateObj.state) && stateObj.attributes && 'query_stage' in stateObj.attributes) { - return stateObj._stateDisplay + ' (' + stateObj.attributes.query_stage + ')'; - } - return stateObj._stateDisplay; - } - } - - return stateObj._stateDisplay; -}; - window.hassUtil.isComponentLoaded = function (hass, component) { return hass && hass.config.core.components.indexOf(component) !== -1; }; diff --git a/src/util/media-player-model.html b/src/util/media-player-model.html index bebd18ba94..e1a0b032ff 100644 --- a/src/util/media-player-model.html +++ b/src/util/media-player-model.html @@ -109,12 +109,11 @@ /* eslint-enable no-bitwise */ - addGetter('primaryText', function () { - return this.stateObj.attributes.media_title || - window.hassUtil.computeStateState(this.stateObj); + addGetter('primaryTitle', function () { + return this.stateObj.attributes.media_title; }); - addGetter('secondaryText', function () { + addGetter('secondaryTitle', function () { if (this.isMusic) { return this.stateObj.attributes.media_artist; } else if (this.isTVShow) {