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) {