Panel translation fragments (#691)

* Move flatten to separate gulp task

* Split translation fragments into separate files

* Load translation fragments when switching panels

* Fix gulpfile lint

* Move app-location to home-assistant.html

* Compute panel navigation in home-assistant.html

* Only pass down panelUrl from home-assistant.html

* Store panelUrl on hass

* Store computed panel on hass object

* Revert "Store computed panel on hass object"

This reverts commit 0f150b1faa2b91588a432ab346821b633b349d1a.

IMO this didn't really make the code cleaner. Just wanted to see how it
would look. Keeping it here as a reverted commit in case we do want to
use it.
This commit is contained in:
Adam Mills 2017-12-05 09:12:42 -05:00 committed by GitHub
parent 2765c88d3f
commit e0a63a2ee3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 207 additions and 95 deletions

View File

@ -9,7 +9,17 @@ const rename = require('gulp-rename');
const transform = require('gulp-json-transform');
const inDir = 'translations';
const outDir = 'build-translations';
const workDir = 'build-translations';
const fullDir = workDir + '/full';
const coreDir = workDir + '/core';
const outDir = workDir + '/output';
// Panel translations which should be split from the core translations. These
// should mirror the fragment definitions in polymer.json, so that we load
// additional resources at equivalent points.
const TRANSLATION_FRAGMENTS = [
'shopping-list',
];
const tasks = [];
@ -29,6 +39,20 @@ function flatten(data) {
return recursiveFlatten('', data);
}
function emptyFilter(data) {
const newData = {};
Object.keys(data).forEach((key) => {
if (data[key]) {
if (typeof (data[key]) === 'object') {
newData[key] = emptyFilter(data[key]);
} else {
newData[key] = data[key];
}
}
});
return newData;
}
/**
* Replace Lokalise key placeholders with their actual values.
*
@ -73,7 +97,7 @@ gulp.task(taskName, function () {
return lokalise_transform(data, data);
}))
.pipe(rename('translationMaster.json'))
.pipe(gulp.dest(outDir));
.pipe(gulp.dest(workDir));
});
tasks.push(taskName);
@ -90,36 +114,80 @@ gulp.task(taskName, ['build-master-translation'], function () {
// than a base translation + region.
const tr = path.basename(file.history[0], '.json');
const subtags = tr.split('-');
const src = [outDir + '/translationMaster.json'];
const src = [workDir + '/translationMaster.json'];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join('-');
src.push(inDir + '/' + lang + '.json');
}
return gulp.src(src)
.pipe(transform(function (data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
}))
.pipe(transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out empty strings or other falsey values before merging
if (data[key]) newData[key] = value;
});
return newData;
}))
.pipe(transform(data => emptyFilter(data)))
.pipe(merge({
fileName: tr + '.json',
}))
.pipe(minify())
.pipe(gulp.dest(outDir));
.pipe(gulp.dest(fullDir));
}));
});
tasks.push(taskName);
taskName = 'build-translation-fingerprints';
const splitTasks = [];
TRANSLATION_FRAGMENTS.forEach((fragment) => {
taskName = 'build-translation-fragment-' + fragment;
gulp.task(taskName, ['build-merged-translations'], function () {
// Return only the translations for this fragment.
return gulp.src(fullDir + '/*.json')
.pipe(transform(data => ({
ui: {
panel: {
[fragment]: data.ui.panel[fragment],
},
},
})))
.pipe(gulp.dest(workDir + '/' + fragment));
});
tasks.push(taskName);
splitTasks.push(taskName);
});
taskName = 'build-translation-core';
gulp.task(taskName, ['build-merged-translations'], function () {
return gulp.src(outDir + '/!(translationFingerprints|translationMaster).json')
// Remove the fragment translations from the core translation.
return gulp.src(fullDir + '/*.json')
.pipe(transform((data) => {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
return data;
}))
.pipe(gulp.dest(coreDir));
});
tasks.push(taskName);
splitTasks.push(taskName);
taskName = 'build-flattened-translations';
gulp.task(taskName, splitTasks, function () {
// Flatten the split versions of our translations, and move them into outDir
return gulp.src(
TRANSLATION_FRAGMENTS.map(fragment => workDir + '/' + fragment + '/*.json')
.concat(coreDir + '/*.json'),
{ base: workDir },
)
.pipe(transform(function (data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
}))
.pipe(minify())
.pipe(rename((filePath) => {
if (filePath.dirname === 'core') {
filePath.dirname = '';
}
}))
.pipe(gulp.dest(outDir));
});
tasks.push(taskName);
taskName = 'build-translation-fingerprints';
gulp.task(taskName, ['build-flattened-translations'], function () {
return gulp.src(outDir + '/**/*.json')
.pipe(rename({
extname: '',
}))
@ -130,12 +198,25 @@ gulp.task(taskName, ['build-merged-translations'], function () {
}))
.pipe(hash.manifest('translationFingerprints.json'))
.pipe(transform(function (data) {
Object.keys(data).forEach((key) => {
data[key] = { fingerprint: data[key] };
// After generating fingerprints of our translation files, consolidate
// all translation fragment fingerprints under the translation name key
const newData = {};
Object.entries(data).forEach(([key, value]) => {
const parts = key.split('/');
let translation = key;
if (parts.length === 2) {
translation = parts[1];
}
if (!(translation in newData)) {
newData[translation] = {
fingerprints: {},
};
}
newData[translation].fingerprints[key] = value;
});
return data;
return newData;
}))
.pipe(gulp.dest(outDir));
.pipe(gulp.dest(workDir));
});
tasks.push(taskName);
@ -143,13 +224,13 @@ taskName = 'build-translations';
gulp.task(taskName, ['build-translation-fingerprints'], function () {
return gulp.src([
'src/translations/translationMetadata.json',
outDir + '/translationFingerprints.json',
workDir + '/translationFingerprints.json',
])
.pipe(merge({}))
.pipe(transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out empty strings or other falsey values before merging
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
@ -159,9 +240,13 @@ gulp.task(taskName, ['build-translation-fingerprints'], function () {
});
return newData;
}))
.pipe(transform(data => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
})))
.pipe(insert.wrap('<script>\nwindow.translationMetadata = ', ';\n</script>'))
.pipe(rename('translationMetadata.html'))
.pipe(gulp.dest(outDir));
.pipe(gulp.dest(workDir));
});
tasks.push(taskName);

View File

@ -122,7 +122,7 @@ class HaConfigCore extends Polymer.Element {
computeIsTranslationLoaded(hass) {
return hass.translationMetadata &&
Object.keys(hass.translationMetadata).length;
Object.keys(hass.translationMetadata.translations).length;
}
computeIsThemesLoaded(hass) {

View File

@ -60,9 +60,9 @@ class HaConfigSectionTranslation extends
if (!hass || !hass.translationMetadata) {
return [];
}
return Object.keys(hass.translationMetadata).map(key => ({
return Object.keys(hass.translationMetadata.translations).map(key => ({
tag: key,
nativeName: hass.translationMetadata[key].nativeName,
nativeName: hass.translationMetadata.translations[key].nativeName,
}));
}

View File

@ -12,6 +12,7 @@
<link rel="import" href="../../bower_components/paper-listbox/paper-listbox.html">
<link rel="import" href="../../bower_components/paper-item/paper-item.html">
<link rel='import' href='../..//src/util/hass-mixins.html'>
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/components/ha-start-voice-button.html">
@ -65,7 +66,7 @@
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
<div main-title>Shopping List</div>
<div main-title>[[haLocalize('panel', 'shopping_list')]]</div>
<ha-start-voice-button hass='[[hass]]' can-listen='{{canListen}}'></ha-start-voice-button>
<paper-menu-button
horizontal-align="right"
@ -79,7 +80,7 @@
<paper-listbox slot="dropdown-content">
<paper-item
on-tap="_clearCompleted"
>Clear completed</paper-item>
>[[haLocalize('ui.panel.shopping-list', 'clear_completed')]]</paper-item>
</paper-listbox>
</paper-menu-button>
</app-toolbar>
@ -96,7 +97,7 @@
<paper-item-body>
<paper-input
id='addBox'
placeholder="Add item"
placeholder="[[haLocalize('ui.panel.shopping-list', 'add_item')]]"
on-keydown='_addKeyPress'
no-label-float
></paper-input>
@ -123,7 +124,7 @@
</template>
</paper-card>
<div class='tip' hidden$='[[!canListen]]'>
Tap the microphone on the top right and say "Add candy to my shopping list"
[[haLocalize('ui.panel.shopping-list', 'microphone_tip')]]
</div>
</div>
</app-header-layout>
@ -131,7 +132,10 @@
</dom-module>
<script>
class HaPanelShoppingList extends Polymer.Element {
/*
* @appliesMixin window.hassMixins.LocalizeMixin
*/
class HaPanelShoppingList extends window.hassMixins.LocalizeMixin(Polymer.Element) {
static get is() { return 'ha-panel-shopping-list'; }
static get properties() {

View File

@ -28,8 +28,7 @@ mkdir $OUTPUT_DIR_ES5/panels
cp build-es5/panels/*.html $OUTPUT_DIR_ES5/panels
# Translations
mkdir $OUTPUT_DIR/translations
cp build-translations/*.json $OUTPUT_DIR/translations
cp -r build-translations/output $OUTPUT_DIR/translations
# Local Roboto
cp -r bower_components/font-roboto-local/fonts $OUTPUT_DIR
@ -60,6 +59,7 @@ gzip -f -n -k -9 \
*.js \
./panels/*.html \
./translations/*.json \
./translations/*/*.json \
./fonts/roboto/*.ttf \
./fonts/robotomono/*.ttf
cd ..

View File

@ -101,7 +101,7 @@
<paper-icon-button icon='mdi:chevron-left' hidden$='[[narrow]]' on-tap='toggleMenu'></paper-icon-button>
</app-toolbar>
<paper-listbox attr-for-selected='data-panel' selected='[[route.panel]]'>
<paper-listbox attr-for-selected='data-panel' selected='[[hass.panelUrl]]'>
<paper-icon-item on-tap='menuClicked' data-panel='states'>
<iron-icon slot="item-icon" icon='mdi:apps'></iron-icon>
<span class='item-text'>[[haLocalize('panel', 'states')]]</span>
@ -171,19 +171,13 @@ class HaSidebar extends
hass: {
type: Object,
},
menuShown: {
type: Boolean,
},
menuSelected: {
type: String,
},
narrow: Boolean,
route: Object,
panels: {
type: Array,
computed: 'computePanels(hass)',

View File

@ -5,11 +5,13 @@
<link rel='import' href='./util/roboto.html'>
<link rel='import' href='../bower_components/paper-styles/typography.html'>
<link rel='import' href='../bower_components/iron-flex-layout/iron-flex-layout-classes.html'>
<link rel='import' href='../bower_components/app-route/app-route.html'>
<link rel='import' href='../bower_components/app-route/app-location.html'>
<!--polyfill for paper-dropdown-->
<link rel="import" href="../bower_components/neon-animation/web-animations.html">
<link rel='import' href='../build-translations/translationMetadata.html' />
<link rel='import' href='./util/hass-translation.html'>
<link rel='import' href='./util/hass-util.html'>
<link rel='import' href='./util/ha-pref-storage.html'>
@ -24,6 +26,12 @@
<template>
<ha-pref-storage hass='[[hass]]' id='storage'></ha-pref-storage>
<notification-manager id='notifications' hass='[[hass]]'></notification-manager>
<app-location route="{{route}}"></app-location>
<app-route
route="{{route}}"
pattern="/:panel"
data="{{routeData}}"
></app-route>
<template is='dom-if' if='[[showMain]]' restamp>
<home-assistant-main
on-hass-more-info='handleMoreInfo'
@ -31,6 +39,7 @@
on-hass-notification='handleNotification'
on-hass-logout='handleLogout'
hass='[[hass]]'
route='{{route}}'
></home-assistant-main>
</template>
@ -75,6 +84,13 @@ class HomeAssistant extends Polymer.Element {
type: Boolean,
computed: 'computeShowMain(hass)',
},
route: Object,
routeData: Object,
panelUrl: {
type: String,
computed: 'computePanelUrl(routeData)',
observer: 'panelUrlChanged',
},
};
}
@ -95,12 +111,23 @@ class HomeAssistant extends Polymer.Element {
|| (hass && hass.connection && (!hass.states || !hass.config)));
}
loadResources() {
window.getTranslation().then((result) => {
this._updateHass({
language: result.language,
resources: result.resources,
});
loadResources(fragment) {
window.getTranslation(fragment).then((result) => {
this._updateResources(result.language, result.data);
});
}
_updateResources(language, data) {
// Update the language in hass, and update the resources with the newly
// loaded resources. This merges the new data on top of the old data for
// this language, so that the full translation set can be loaded across
// multiple fragments.
this._updateHass({
language: language,
resources: {
[language]: Object.assign({}, this.hass
&& this.hass.resources && this.hass.resources[language], data),
},
});
}
@ -130,6 +157,7 @@ class HomeAssistant extends Polymer.Element {
states: null,
config: null,
themes: null,
panelUrl: this.panelUrl,
// If language and resources are already loaded, don't discard them
language: (this.hass && this.hass.language) || null,
@ -219,6 +247,15 @@ class HomeAssistant extends Polymer.Element {
};
}
computePanelUrl(routeData) {
return (routeData && routeData.panel) || 'states';
}
panelUrlChanged(newPanelUrl) {
this._updateHass({ panelUrl: newPanelUrl });
this.loadTranslationFragment(newPanelUrl);
}
handleConnectionPromise(prom) {
if (!prom) return;
@ -268,6 +305,14 @@ class HomeAssistant extends Polymer.Element {
this._updateHass({ selectedLanguage: event.detail.language });
this.$.storage.storeState();
this.loadResources();
this.loadTranslationFragment(this.panelUrl);
}
loadTranslationFragment(panelUrl) {
if (this.hass.translationMetadata
&& this.hass.translationMetadata.fragments.includes(panelUrl)) {
this.loadResources(panelUrl);
}
}
_updateHass(obj) {

View File

@ -2,10 +2,7 @@
<link rel='import' href='../../bower_components/paper-drawer-panel/paper-drawer-panel.html'>
<link rel='import' href='../../bower_components/iron-media-query/iron-media-query.html'>
<link rel='import' href='../../bower_components/iron-pages/iron-pages.html'>
<link rel='import' href='../../bower_components/app-route/app-route.html'>
<link rel='import' href='../../bower_components/app-route/app-location.html'>
<link rel='import' href='../layouts/partial-cards.html'>
<link rel='import' href='../layouts/partial-panel-resolver.html'>
@ -26,13 +23,6 @@
</style>
<more-info-dialog hass='[[hass]]'></more-info-dialog>
<ha-url-sync hass='[[hass]]'></ha-url-sync>
<app-location route="{{route}}"></app-location>
<app-route
route="{{route}}"
pattern="/:panel"
data="{{routeData}}"
tail="{{routeTail}}"
></app-route>
<app-route
route="{{route}}"
pattern="/states"
@ -47,20 +37,19 @@
<paper-drawer-panel id='drawer'
force-narrow='[[computeForceNarrow(narrow, dockedSidebar)]]'
responsive-width='0' disable-swipe='[[_computeDisableSwipe(routeData)]]'
disable-edge-swipe='[[_computeDisableSwipe(routeData)]]'>
responsive-width='0' disable-swipe='[[_computeDisableSwipe(hass)]]'
disable-edge-swipe='[[_computeDisableSwipe(hass)]]'>
<ha-sidebar
slot="drawer"
narrow='[[narrow]]'
hass='[[hass]]'
route='[[routeData]]'
></ha-sidebar>
<iron-pages
slot="main"
attr-for-selected='id'
fallback-selection='panel-resolver'
selected='[[_computeSelected(routeData)]]'
selected='[[hass.panelUrl]]'
selected-attribute='panel-visible'
>
<partial-cards
@ -96,17 +85,12 @@
static get properties() {
return {
hass: Object,
narrow: Boolean,
route: {
type: Object,
observer: '_routeChanged',
},
routeData: Object,
routeTail: Object,
statesRouteTail: Object,
dockedSidebar: {
type: Boolean,
computed: 'computeDockedSidebar(hass)',
@ -163,12 +147,8 @@
return hass.dockedSidebar;
}
_computeSelected(routeData) {
return routeData.panel || 'states';
}
_computeDisableSwipe(routeData) {
return NON_SWIPABLE_PANELS.indexOf(routeData.panel) !== -1;
_computeDisableSwipe(hass) {
return NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
}
}

View File

@ -1,6 +1,7 @@
<link rel='import' href='../../bower_components/polymer/polymer-element.html'>
<link rel='import' href='../../bower_components/app-route/app-route.html'>
<link rel='import' href='../util/hass-mixins.html'>
<link rel="import" href="./hass-loading-screen.html">
<dom-module id='partial-panel-resolver'>
@ -50,38 +51,28 @@ class PartialPanelResolver extends Polymer.Element {
value: false,
observer: 'updateAttributes',
},
route: Object,
routeData: Object,
routeTail: {
type: Object,
observer: 'updateAttributes',
},
resolved: {
type: Boolean,
value: false,
},
errorLoading: {
type: Boolean,
value: false,
},
panel: {
type: Object,
computed: 'computeCurrentPanel(hass, routeData)',
computed: 'computeCurrentPanel(hass)',
observer: 'panelChanged',
},
};
}
computeCurrentPanel(hass, routeData) {
return routeData ? hass.config.panels[routeData.panel] : null;
}
panelChanged(panel) {
if (!panel) {
if (this.$.panel.lastChild) {
@ -123,6 +114,10 @@ class PartialPanelResolver extends Polymer.Element {
customEl.showMenu = this.showMenu;
customEl.route = this.routeTail;
}
computeCurrentPanel(hass) {
return hass.config.panels[hass.panelUrl];
}
}
customElements.define(PartialPanelResolver.is, PartialPanelResolver);

View File

@ -218,5 +218,14 @@
"home": "[%key:state::device_tracker::home%]",
"not_home": "[%key:state::device_tracker::not_home%]"
}
},
"ui": {
"panel": {
"shopping-list": {
"clear_completed": "Clear completed",
"add_item": "Add item",
"microphone_tip": "Tap the microphone on the top right and say “Add candy to my shopping list”"
}
}
}
}

View File

@ -6,7 +6,7 @@ function getActiveTranslation() {
// report languages with specific cases.
const lookup = {};
/* eslint-disable no-undef */
Object.keys(window.translationMetadata).forEach((tr) => {
Object.keys(window.translationMetadata.translations).forEach((tr) => {
lookup[tr.toLowerCase()] = tr;
});
@ -60,16 +60,18 @@ function getActiveTranslation() {
// when DOM is created in Polymer. Even a cache lookup creates noticable latency.
const translations = {};
window.getTranslation = function (translationInput) {
window.getTranslation = function (fragment, translationInput) {
const translation = translationInput || getActiveTranslation();
const metadata = window.translationMetadata[translation];
const metadata = window.translationMetadata.translations[translation];
if (!metadata) {
if (translationInput !== 'en') {
return window.getTranslation('en');
return window.getTranslation(fragment, 'en');
}
return Promise.reject(new Error('Language en not found in metadata'));
}
const translationFingerprint = metadata.fingerprint;
const translationFingerprint = metadata.fingerprints[
fragment ? `${fragment}/${translation}` : translation
];
// Create a promise to fetch translation from the server
if (!translations[translationFingerprint]) {
@ -77,15 +79,13 @@ window.getTranslation = function (translationInput) {
fetch(`/static/translations/${translationFingerprint}`, { credentials: 'include' })
.then(response => response.json()).then(data => ({
language: translation,
resources: {
[translation]: data
},
data: data,
}))
.catch((error) => {
delete translations[translationFingerprint];
if (translationInput !== 'en') {
// Couldn't load selected translation. Try a fall back to en before failing.
return window.getTranslation('en');
return window.getTranslation(fragment, 'en');
}
return Promise.reject(error);
});