diff --git a/Gruntfile.js b/Gruntfile.js index 161b046..1a48eda 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -3,7 +3,7 @@ module.exports = function exports(grunt) { grunt.loadTasks('tasks'); - grunt.registerTask('default', ['lint', 'copy', 'sass:dist', 'postcss', 'requirejs:default', 'inlinedata', 'cachebreaker', 'inline', 'htmlmin', 'clean:release']); + grunt.registerTask('default', ['lint', 'copy', 'sass:dist', 'postcss', 'requirejs:default', 'inlinedata', 'cachebreaker', 'inline', 'htmlmin', 'json-minify', 'clean:release']); grunt.registerTask('lint', ['sasslint', 'eslint']); - grunt.registerTask('serve', ['lint', 'copy', 'sass:dev', 'postcss', 'requirejs:dev', 'inlinedata', 'htmlmin', 'browserSync', 'watch']); + grunt.registerTask('serve', ['lint', 'copy', 'sass:dev', 'postcss', 'requirejs:dev', 'inlinedata', 'htmlmin', 'json-minify', 'browserSync', 'watch']); }; diff --git a/README.md b/README.md index 362722f..fd5e755 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - Updates selected node or list (incl. image stats cache-breaker) - not only overview tables - Zoom level if you click a node (`nodeZoom`) - Zoom level 22 available, but it is to close for a click - Formatted Code +- Translation support - https://crowdin.com/project/meshviewer - Grunt inline for some css and js - less requests - Icon font with only needed icons - Upgrade to grunt v1.x (Tested with Node.js 4 LTS,6 LTS,7 Linux,OSX,W**) @@ -154,7 +155,7 @@ This option allows to show node statistics depending on following case-sensitive - `caption` is shown, if `thumbnail` is not present (no thumbnail in infobox) To insert current node-id in either `href`, `thumbnail` or `caption` -you can use the case-sensitive template string `{NODE_ID}`, `{NODE_NAME}` and `{TIME}` as cache-breaker. +you can use the case-sensitive template string `{NODE_ID}`, `{NODE_NAME}`, `{LOCALE}` and `{TIME}` as cache-breaker. Examples for `nodeInfos`: @@ -207,7 +208,7 @@ This option allows to show link statistics depending on the following case-sensi - `caption` is shown, if `thumbnail` is not present (no thumbnail in infobox) To insert the source or target node-id in either `href`, `thumbnail` or `caption` -you can use the case-sensitive template strings `{SOURCE}`, `{TARGET}` and `{TIME}` as cache-breaker. +you can use the case-sensitive template strings `{SOURCE}`, `{LOCALE}`, `{TARGET}` and `{TIME}` as cache-breaker. "linkInfos": [ { "href": "stats/dashboard/db/links?var-source={SOURCE}&var-target={TARGET}", @@ -232,6 +233,18 @@ Example for `siteNames`: { "site": "ffgt", "name": "Gothamcity" }, { "site": "ffal", "name": "Atlantis" } ] + + +## supportedLocale (array) + +Add supported locale (with matching language file in locales/*.json) and it will be matched against the browser language setting. Fallback is the first language in the array. + +Example for `supportedLocale`: + + "supportedLocale": [ + "en", + "de" + ] ## Sponsoring / Supporting - [BrowserStack](https://www.browserstack.com/) for providing a awesome testing service for hundreds of browsers diff --git a/app.js b/app.js index 2d9b159..6e8875f 100644 --- a/app.js +++ b/app.js @@ -3,10 +3,11 @@ require.config({ baseUrl: 'lib', paths: { + 'polyglot': '../node_modules/node-polyglot/build/polyglot', 'leaflet': '../node_modules/leaflet/dist/leaflet', 'leaflet.label': '../node_modules/leaflet-label/dist/leaflet.label', 'chroma-js': '../node_modules/chroma-js/chroma.min', - 'moment': '../node_modules/moment', + 'moment': '../node_modules/moment/moment', 'tablesort': '../node_modules/tablesort/src/tablesort', 'd3': '../node_modules/d3/d3.min', 'virtual-dom': '../node_modules/virtual-dom/dist/virtual-dom', diff --git a/config.json b/config.json index 4d2665c..a26bd21 100644 --- a/config.json +++ b/config.json @@ -110,5 +110,9 @@ "site": "ffrgb", "name": "Regensburg" } + ], + "supportedLocale": [ + "en", + "de" ] } diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..d043b23 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /locale/en.json + translation: /locale/%two_letters_code%.json diff --git a/lib/about.js b/lib/about.js index 7e2f078..ecad904 100644 --- a/lib/about.js +++ b/lib/about.js @@ -3,10 +3,7 @@ define(function () { return function () { this.render = function render(d) { - d.innerHTML = '

Über Meshviewer

' + - - '

Mit Doppelklick und Shift+Doppelklick kann man in der Karte ' + - 'auch zoomen.

' + + d.innerHTML = _.t('sidebar.aboutInfo') + '

AGPL 3

' + diff --git a/lib/gui.js b/lib/gui.js index f38c422..815a839 100644 --- a/lib/gui.js +++ b/lib/gui.js @@ -81,8 +81,8 @@ define(['chroma-js', 'map', 'sidebar', 'tabs', 'container', 'legend', var tabs = new Tabs(); var overview = new Container(); var legend = new Legend(config); - var newnodeslist = new SimpleNodelist('new', 'firstseen', router, 'Neue Knoten'); - var lostnodeslist = new SimpleNodelist('lost', 'lastseen', router, 'Verschwundene Knoten'); + var newnodeslist = new SimpleNodelist('new', 'firstseen', router, _.t('node.new')); + var lostnodeslist = new SimpleNodelist('lost', 'lastseen', router, _.t('node.missing')); var nodelist = new Nodelist(router); var linklist = new Linklist(linkScale, router); var statistics = new Proportions(config, fanout); @@ -106,11 +106,11 @@ define(['chroma-js', 'map', 'sidebar', 'tabs', 'container', 'legend', header.add(filterGUI); sidebar.add(tabs); - tabs.add('Aktuelles', overview); - tabs.add('Knoten', nodelist); - tabs.add('Verbindungen', linklist); - tabs.add('Statistiken', statistics); - tabs.add('Über', about); + tabs.add('sidebar.actual', overview); + tabs.add('node.nodes', nodelist); + tabs.add('node.links', linklist); + tabs.add('sidebar.stats', statistics); + tabs.add('sidebar.about', about); router.addTarget(title); router.addTarget(infobox); diff --git a/lib/infobox/link.js b/lib/infobox/link.js index 8a7f203..1dd5c76 100644 --- a/lib/infobox/link.js +++ b/lib/infobox/link.js @@ -6,6 +6,7 @@ define(['helper'], function (helper) { subst['{SOURCE}'] = source; subst['{TARGET}'] = target; subst['{TIME}'] = time; + subst['{LOCALE}'] = _.locale(); return helper.showStat(o, subst); } @@ -37,11 +38,12 @@ define(['helper'], function (helper) { var attributes = document.createElement('table'); attributes.classList.add('attributes'); - helper.attributeEntry(attributes, 'TQ', helper.showTq(d)); - helper.attributeEntry(attributes, 'Entfernung', helper.showDistance(d)); + helper.attributeEntry(attributes, 'node.tq', helper.showTq(d)); + helper.attributeEntry(attributes, 'node.distance', helper.showDistance(d)); var hw1 = unknown ? null : helper.dictGet(d.source.node.nodeinfo, ['hardware', 'model']); var hw2 = helper.dictGet(d.target.node.nodeinfo, ['hardware', 'model']); - helper.attributeEntry(attributes, 'Hardware', (hw1 !== null ? hw1 : 'unbekannt') + ' – ' + (hw2 !== null ? hw2 : 'unbekannt')); + helper.attributeEntry(attributes, 'node.hardware', (hw1 !== null ? hw1 : _.t('unknown')) + ' – ' + (hw2 !== null ? hw2 : _.t('unknown'))); + el.appendChild(attributes); if (config.linkInfos) { diff --git a/lib/infobox/location.js b/lib/infobox/location.js index 909df48..802893b 100644 --- a/lib/infobox/location.js +++ b/lib/infobox/location.js @@ -3,10 +3,10 @@ define(['helper'], function (helper) { return function (config, el, router, d) { var sidebarTitle = document.createElement('h2'); - sidebarTitle.textContent = 'Location: ' + d.toString(); + sidebarTitle.textContent = _.t('location.location') + ': ' + d.toString(); el.appendChild(sidebarTitle); - helper.getJSON(config.reverseGeocodingApi + '?format=json&lat=' + d.lat + '&lon=' + d.lng + '&zoom=18&addressdetails=0') + helper.getJSON(config.reverseGeocodingApi + '?format=json&lat=' + d.lat + '&lon=' + d.lng + '&zoom=18&addressdetails=0&accept-language=' + _.locale()) .then(function (result) { if (result.display_name) { sidebarTitle.textContent = result.display_name; @@ -16,12 +16,12 @@ define(['helper'], function (helper) { var editLat = document.createElement('input'); editLat.type = 'text'; editLat.value = d.lat.toFixed(9); - el.appendChild(createBox('lat', 'Breitengrad', editLat)); + el.appendChild(createBox('lat', _.t('location.latitude'), editLat)); var editLng = document.createElement('input'); editLng.type = 'text'; editLng.value = d.lng.toFixed(9); - el.appendChild(createBox('lng', 'Längengrad', editLng)); + el.appendChild(createBox('lng', _.t('location.longitude'), editLng)); var editUci = document.createElement('textarea'); editUci.value = @@ -40,7 +40,7 @@ define(['helper'], function (helper) { box.appendChild(heading); var btn = document.createElement('button'); btn.classList.add('ion-ios-copy'); - btn.title = 'Kopieren'; + btn.title = _.t('location.copy'); btn.onclick = function onclick() { copy2clip(inputElem.id); }; diff --git a/lib/infobox/node.js b/lib/infobox/node.js index 6ed6f8c..c98b204 100644 --- a/lib/infobox/node.js +++ b/lib/infobox/node.js @@ -1,5 +1,5 @@ -define(['chroma-js', 'moment/moment', 'tablesort', 'helper', 'moment/locale/de'], - function (chroma, moment, tablesort, helper) { +define(['chroma-js', 'moment', 'tablesort', 'helper'], + function (chroma, moment, Tablesort, helper) { 'use strict'; function showGeoURI(d) { @@ -19,9 +19,15 @@ define(['chroma-js', 'moment/moment', 'tablesort', 'helper', 'moment/locale/de'] return function (el) { el.classList.add(d.flags.unseen ? 'unseen' : (d.flags.online ? 'online' : 'offline')); if (d.flags.online) { - el.textContent = 'online, letzte Nachricht ' + d.lastseen.fromNow() + ' (' + d.lastseen.format('DD.MM.YYYY, H:mm:ss') + ')'; + el.textContent = _.t('node.lastOnline', { + time: d.lastseen.fromNow(), + date: d.lastseen.format('DD.MM.YYYY, H:mm:ss') + }); } else { - el.textContent = 'offline, letzte Nachricht ' + d.lastseen.fromNow() + ' (' + d.lastseen.format('DD.MM.YYYY, H:mm:ss') + ')'; + el.textContent = _.t('node.lastOffline', { + time: d.lastseen.fromNow(), + date: d.lastseen.format('DD.MM.YYYY, H:mm:ss') + }); } }; } @@ -72,7 +78,7 @@ define(['chroma-js', 'moment/moment', 'tablesort', 'helper', 'moment/locale/de'] } return function (el) { - el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : 'keine')); + el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : _.t('none'))); el.appendChild(document.createElement('br')); var span = document.createElement('span'); @@ -169,14 +175,15 @@ define(['chroma-js', 'moment/moment', 'tablesort', 'helper', 'moment/locale/de'] return undefined; } - return au.enabled ? 'aktiviert (' + au.branch + ')' : 'deaktiviert'; + return au.enabled ? _.t('node.activated', {branch: au.branch}) : _.t('node.deactivated'); } function showStatImg(o, d) { var subst = {}; - subst['{NODE_ID}'] = d.nodeinfo.node_id ? d.nodeinfo.node_id : 'unknown'; - subst['{NODE_NAME}'] = d.nodeinfo.hostname ? d.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_') : 'unknown'; + subst['{NODE_ID}'] = d.nodeinfo.node_id ? d.nodeinfo.node_id : _.t('unknown'); + subst['{NODE_NAME}'] = d.nodeinfo.hostname ? d.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_') : _.t('unknown'); subst['{TIME}'] = d.lastseen.format('DDMMYYYYHmmss'); + subst['{LOCALE}'] = _.locale(); return helper.showStat(o, subst); } @@ -189,29 +196,29 @@ define(['chroma-js', 'moment/moment', 'tablesort', 'helper', 'moment/locale/de'] var attributes = document.createElement('table'); attributes.classList.add('attributes'); - helper.attributeEntry(attributes, 'Status', showStatus(d)); - helper.attributeEntry(attributes, 'Gateway', d.flags.gateway ? 'ja' : null); - helper.attributeEntry(attributes, 'Koordinaten', showGeoURI(d)); + helper.attributeEntry(attributes, 'node.status', showStatus(d)); + helper.attributeEntry(attributes, 'node.gateway', d.flags.gateway ? 'ja' : null); + helper.attributeEntry(attributes, 'node.coordinates', showGeoURI(d)); if (config.nodeInfobox && config.nodeInfobox.contact) { - helper.attributeEntry(attributes, 'Kontakt', helper.dictGet(d.nodeinfo, ['owner', 'contact'])); + helper.attributeEntry(attributes, 'node.contact', helper.dictGet(d.nodeinfo, ['owner', 'contact'])); } - helper.attributeEntry(attributes, 'Hardware', helper.dictGet(d.nodeinfo, ['hardware', 'model'])); - helper.attributeEntry(attributes, 'Primäre MAC', helper.dictGet(d.nodeinfo, ['network', 'mac'])); - helper.attributeEntry(attributes, 'Node ID', helper.dictGet(d.nodeinfo, ['node_id'])); - helper.attributeEntry(attributes, 'Firmware', showFirmware(d)); - helper.attributeEntry(attributes, 'Site', showSite(d, config)); - helper.attributeEntry(attributes, 'Uptime', showUptime(d)); - helper.attributeEntry(attributes, 'Teil des Netzes', showFirstseen(d)); + helper.attributeEntry(attributes, 'node.hardware', helper.dictGet(d.nodeinfo, ['hardware', 'model'])); + helper.attributeEntry(attributes, 'node.primaryMac', helper.dictGet(d.nodeinfo, ['network', 'mac'])); + helper.attributeEntry(attributes, 'node.id', helper.dictGet(d.nodeinfo, ['node_id'])); + helper.attributeEntry(attributes, 'node.firmware', showFirmware(d)); + helper.attributeEntry(attributes, 'node.site', showSite(d, config)); + helper.attributeEntry(attributes, 'node.uptime', showUptime(d)); + helper.attributeEntry(attributes, 'node.firstSeen', showFirstseen(d)); if (config.nodeInfobox && config.nodeInfobox.hardwareUsage) { - helper.attributeEntry(attributes, 'Systemlast', showLoad(d)); - helper.attributeEntry(attributes, 'Arbeitsspeicher', showRAM(d)); + helper.attributeEntry(attributes, 'node.systemLoad', showLoad(d)); + helper.attributeEntry(attributes, 'node.ram', showRAM(d)); } - helper.attributeEntry(attributes, 'IP Adressen', showIPs(d)); - helper.attributeEntry(attributes, 'Gewähltes Gateway', helper.dictGet(d.statistics, ['gateway'])); - helper.attributeEntry(attributes, 'Autom. Updates', showAutoupdate(d)); - helper.attributeEntry(attributes, 'Clients', showClients(d)); + helper.attributeEntry(attributes, 'node.ipAddresses', showIPs(d)); + helper.attributeEntry(attributes, 'node.selectedGateway', helper.dictGet(d.statistics, ['gateway'])); + helper.attributeEntry(attributes, 'node.update', showAutoupdate(d)); + helper.attributeEntry(attributes, 'node.clients', showClients(d)); el.appendChild(attributes); @@ -226,7 +233,7 @@ define(['chroma-js', 'moment/moment', 'tablesort', 'helper', 'moment/locale/de'] if (d.neighbours.length > 0) { var h3 = document.createElement('h3'); - h3.textContent = 'Links (' + d.neighbours.length + ')'; + h3.textContent = _.t('node.link', d.neighbours.length) + '(' + d.neighbours.length + ')'; el.appendChild(h3); var table = document.createElement('table'); @@ -238,16 +245,16 @@ define(['chroma-js', 'moment/moment', 'tablesort', 'helper', 'moment/locale/de'] tr.appendChild(th1); var th2 = document.createElement('th'); - th2.textContent = 'Knoten'; + th2.textContent = _.t('node.node', d.neighbours.length); th2.classList.add('sort-default'); tr.appendChild(th2); var th3 = document.createElement('th'); - th3.textContent = 'TQ'; + th3.textContent = _.t('node.tq'); tr.appendChild(th3); var th4 = document.createElement('th'); - th4.textContent = 'Entfernung'; + th4.textContent = _.t('node.distance'); tr.appendChild(th4); thead.appendChild(tr); diff --git a/lib/legend.js b/lib/legend.js index 9cf9878..22fa65d 100644 --- a/lib/legend.js +++ b/lib/legend.js @@ -16,12 +16,11 @@ define(['helper'], function (helper) { return n.flags.gateway; }).map(helper.one)); - stats.textContent = totalNodes + ' Knoten, ' + - 'davon ' + totalOnlineNodes + ' Knoten online ' + - 'mit ' + totalClients + ' Client' + ( totalClients === 1 ? ' ' : 's ' ) + - 'auf ' + totalGateways + ' Gateway' + ( totalGateways === 1 ? '' : 's' ); + stats.textContent = _.t('sidebar.nodes', {total: totalNodes, online: totalOnlineNodes}) + ' ' + + _.t('sidebar.clients', {smart_count: totalClients}) + ' ' + + _.t('sidebar.gateway', {smart_count: totalGateways}); - timestamp.textContent = 'Stand: ' + d.timestamp.format('DD.MM.Y HH:mm'); + timestamp.textContent = _.t('sidebar.lastUpdate') + ': ' + d.timestamp.format('DD.MM.Y HH:mm'); }; self.render = function render(el) { @@ -31,9 +30,9 @@ define(['helper'], function (helper) { var p = document.createElement('p'); p.classList.add('legend'); - p.innerHTML = ' Neuer Knoten' + - ' Knoten ist online' + - ' Knoten ist offline'; + p.innerHTML = ' ' + _.t('sidebar.nodeNew') + '' + + ' ' + _.t('sidebar.nodeOnline') + '' + + ' ' + _.t('sidebar.nodeOffline') + ''; el.appendChild(p); p.appendChild(document.createElement('br')); diff --git a/lib/linklist.js b/lib/linklist.js index 67503ab..4548a4e 100644 --- a/lib/linklist.js +++ b/lib/linklist.js @@ -6,19 +6,19 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) { } var headings = [{ - name: 'Knoten', + name: 'node.nodes', sort: function (a, b) { return linkName(a).localeCompare(linkName(b)); }, reverse: false }, { - name: 'TQ', + name: 'node.tq', sort: function (a, b) { return a.tq - b.tq; }, reverse: true }, { - name: 'Entfernung', + name: 'node.distance', sort: function (a, b) { return (a.distance === undefined ? -1 : a.distance) - (b.distance === undefined ? -1 : b.distance); @@ -42,7 +42,7 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) { this.render = function render(d) { var h2 = document.createElement('h2'); - h2.textContent = 'Verbindungen'; + h2.textContent = _.t('node.links'); d.appendChild(h2); d.appendChild(table.el); diff --git a/lib/main.js b/lib/main.js index 7f11ec1..9dcc039 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,5 +1,5 @@ -define(['moment/moment', 'router', 'leaflet', 'gui', 'helper', 'moment/locale/de'], - function (moment, Router, L, GUI, helper) { +define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper'], + function (Polyglot, moment, Router, L, GUI, helper) { 'use strict'; return function (config) { @@ -150,7 +150,35 @@ define(['moment/moment', 'router', 'leaflet', 'gui', 'helper', 'moment/locale/de }; } - moment.locale('de'); + function setTranslation(json) { + _.extend(json); + + moment.locale(_.locale(), { + longDateFormat: { + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L: 'DD.MM.YYYY', + LL: 'D. MMMM YYYY', + LLL: 'D. MMMM YYYY HH:mm', + LLLL: 'dddd, D. MMMM YYYY HH:mm' + }, + calendar: json.momentjs.calendar, + relativeTime: json.momentjs.relativeTime + }); + } + + var language = navigator.languages && navigator.languages[0] || navigator.language || navigator.userLanguage; + var locale = config.supportedLocale[0]; + config.supportedLocale.some(function (item) { + if (language.indexOf(item) !== -1) { + locale = item; + return true; + } + return false; + }); + + window._ = new Polyglot({locale: locale, allowMissing: true}); + helper.getJSON('locale/' + _.locale() + '.json').then(setTranslation); var router = new Router(); diff --git a/lib/map.js b/lib/map.js index 53268c2..992a0b0 100644 --- a/lib/map.js +++ b/lib/map.js @@ -1,4 +1,6 @@ -define(['map/clientlayer', 'map/labelslayer', 'leaflet', 'moment/moment', 'locationmarker', 'rbush', 'helper', 'leaflet.label', 'moment/locale/de'], +define(['map/clientlayer', 'map/labelslayer', + 'leaflet', 'moment', 'locationmarker', 'rbush', 'helper', + 'leaflet.label'], function (ClientLayer, LabelsLayer, L, moment, LocationMarker, rbush, helper) { 'use strict'; diff --git a/lib/nodelist.js b/lib/nodelist.js index a99a9b6..613e120 100644 --- a/lib/nodelist.js +++ b/lib/nodelist.js @@ -28,28 +28,28 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) { var headings = [{ name: '' }, { - name: 'Knoten', + name: 'node.nodes', sort: function (a, b) { return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname); }, reverse: false - }, { - name: 'Uptime', + }, { + name: 'node.uptime', sort: function (a, b) { return a.uptime - b.uptime; }, reverse: true - }, { - name: '#Links', + }, { + name: 'node.links', sort: function (a, b) { return a.meshlinks - b.meshlinks; }, reverse: true - }, { - name: 'Clients', + }, { + name: 'node.clients', sort: function (a, b) { return ('clients' in a.statistics ? a.statistics.clients : -1) - - ('clients' in b.statistics ? b.statistics.clients : -1); + ('clients' in b.statistics ? b.statistics.clients : -1); }, reverse: true }]; @@ -84,7 +84,7 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) { this.render = function render(d) { var h2 = document.createElement('h2'); - h2.textContent = 'Alle Knoten'; + h2.textContent = _.t('node.all'); d.appendChild(h2); d.appendChild(table.el); diff --git a/lib/proportions.js b/lib/proportions.js index f3dfca2..32c37b0 100644 --- a/lib/proportions.js +++ b/lib/proportions.js @@ -75,7 +75,7 @@ define(['chroma-js', 'virtual-dom', 'filters/genericnode', 'helper'], var c1 = Chroma.contrast(scale(v), 'white'); var c2 = Chroma.contrast(scale(v), 'black'); - var filter = new Filter(name, d[2], d[0], d[3]); + var filter = new Filter(_.t(name), d[2], d[0], d[3]); var a = V.h('a', { href: '#', onclick: addFilter(filter) }, d[0]); @@ -111,7 +111,7 @@ define(['chroma-js', 'virtual-dom', 'filters/genericnode', 'helper'], var fwDict = count(nodes, ['nodeinfo', 'software', 'firmware', 'release']); var hwDict = count(nodes, ['nodeinfo', 'hardware', 'model']); var geoDict = count(nodes, ['nodeinfo', 'location'], function (d) { - return d && d.longitude && d.latitude ? 'ja' : 'nein'; + return d && d.longitude && d.latitude ? _.t('yes') : _.t('no'); }); var autoDict = count(nodes, ['nodeinfo', 'software', 'autoupdater'], function (d) { @@ -120,7 +120,7 @@ define(['chroma-js', 'virtual-dom', 'filters/genericnode', 'helper'], } else if (d.enabled) { return d.branch; } - return '(deaktiviert)'; + return _.t('node.deactivated'); }); var siteDict = count(nodes, ['nodeinfo', 'system', 'site_code'], function (d) { @@ -135,10 +135,10 @@ define(['chroma-js', 'virtual-dom', 'filters/genericnode', 'helper'], return rt; }); - fillTable('Status', statusTable, statusDict.sort(function (a, b) { + fillTable('node.status', statusTable, statusDict.sort(function (a, b) { return b[1] - a[1]; })); - fillTable('Firmware', fwTable, fwDict.sort(function (a, b) { + fillTable('node.firmware', fwTable, fwDict.sort(function (a, b) { if (b[0] < a[0]) { return -1; } @@ -147,28 +147,28 @@ define(['chroma-js', 'virtual-dom', 'filters/genericnode', 'helper'], } return 0; })); - fillTable('Hardware', hwTable, hwDict.sort(function (a, b) { + fillTable('node.hardware', hwTable, hwDict.sort(function (a, b) { return b[1] - a[1]; })); - fillTable('Koordinaten', geoTable, geoDict.sort(function (a, b) { + fillTable('node.visible', geoTable, geoDict.sort(function (a, b) { return b[1] - a[1]; })); - fillTable('Autom. Updates', autoTable, autoDict.sort(function (a, b) { + fillTable('node.update', autoTable, autoDict.sort(function (a, b) { return b[1] - a[1]; })); - fillTable('Site', siteTable, siteDict.sort(function (a, b) { + fillTable('node.site', siteTable, siteDict.sort(function (a, b) { return b[1] - a[1]; })); }; self.render = function render(el) { var h2; - self.renderSingle(el, 'Status', statusTable); - self.renderSingle(el, 'Firmwareversionen', fwTable); - self.renderSingle(el, 'Hardwaremodelle', hwTable); - self.renderSingle(el, 'Auf der Karte sichtbar', geoTable); - self.renderSingle(el, 'Autoupdater', autoTable); - self.renderSingle(el, 'Site', siteTable); + self.renderSingle(el, 'node.status', statusTable); + self.renderSingle(el, 'node.firmware', fwTable); + self.renderSingle(el, 'node.hardware', hwTable); + self.renderSingle(el, 'node.visible', geoTable); + self.renderSingle(el, 'node.update', autoTable); + self.renderSingle(el, 'node.site', siteTable); if (config.globalInfos) { config.globalInfos.forEach(function (globalInfo) { @@ -183,7 +183,7 @@ define(['chroma-js', 'virtual-dom', 'filters/genericnode', 'helper'], self.renderSingle = function renderSingle(el, heading, table) { var h2; h2 = document.createElement('h2'); - h2.textContent = heading; + h2.textContent = _.t(heading); h2.onclick = function onclick() { table.classList.toggle('hidden'); }; diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js index 26c4f5b..6ac17c8 100644 --- a/lib/simplenodelist.js +++ b/lib/simplenodelist.js @@ -1,4 +1,4 @@ -define(['moment/moment', 'virtual-dom', 'helper', 'moment/locale/de'], function (moment, V, helper) { +define(['moment', 'virtual-dom', 'helper'], function (moment, V, helper) { 'use strict'; return function (nodes, field, router, title) { diff --git a/lib/sorttable.js b/lib/sorttable.js index 2fa43d3..9665594 100644 --- a/lib/sorttable.js +++ b/lib/sorttable.js @@ -34,7 +34,7 @@ define(['virtual-dom'], function (V) { properties.className += sortReverse ? ' sort-up' : ' sort-down'; } - return V.h('th', properties, d.name); + return V.h('th', properties, _.t(d.name)); }); var links = data.slice(0).sort(headings[sortIndex].sort); diff --git a/lib/tabs.js b/lib/tabs.js index 9d8220e..f3c13d2 100644 --- a/lib/tabs.js +++ b/lib/tabs.js @@ -34,7 +34,7 @@ define(function () { self.add = function add(title, d) { var li = document.createElement('li'); - li.textContent = title; + li.textContent = _.t(title); li.onclick = switchTab; li.child = d; tabs.appendChild(li); diff --git a/lib/utils/helper.js b/lib/utils/helper.js index 003b06b..ed1aa34 100644 --- a/lib/utils/helper.js +++ b/lib/utils/helper.js @@ -132,9 +132,10 @@ define({ return ''; } + var tr = document.createElement('tr'); var th = document.createElement('th'); - th.textContent = label; + th.textContent = _.t(label); tr.appendChild(th); var td = document.createElement('td'); diff --git a/locale/de.json b/locale/de.json new file mode 100644 index 0000000..9034e42 --- /dev/null +++ b/locale/de.json @@ -0,0 +1,83 @@ +{ + "node":{ + "all":"Alle Knoten", + "nodes":"Knoten", + "uptime":"Laufzeit", + "links":"Verbindungen", + "clients":"Nutzer", + "distance":"Entfernung", + "tq":"TQ", + "lastOnline":"online, letzte Nachricht %{time} (%{date})", + "lastOffline":"offline, letzte Nachricht %{time} (%{date})", + "activated":"aktiviert (%{branch})", + "deactivated":"deaktiviert", + "status":"Status", + "firmware":"Firmware-Version", + "hardware":"Geräte-Modell", + "visible":"Auf der Karte sichtbar", + "update":"Auto-Update", + "site":"Site", + "gateway":"Gateway", + "coordinates":"Koordinaten", + "contact":"Kontakt", + "primaryMac":"Primäre MAC", + "id":"Knoten ID", + "firstSeen":"Erstmals gesehen", + "systemLoad":"Load average", + "ram":"Speicherauslastung", + "ipAddresses":"IP Adressen", + "selectedGateway":"Gewähltes Gateway", + "link":"Verbindung |||| Verbindungen", + "node":"Knoten", + "new":"Neuer Knoten", + "missing":"Verschwundene Knoten" + }, + "location":{ + "location":"Standort", + "latitude":"Breitengrad", + "longitude":"Längengrad", + "copy":"Kopieren" + }, + "sidebar":{ + "nodes":"%{total} Knoten, davon %{online} Knoten online", + "clients":"mit %{smart_count} Nutzer |||| mit %{smart_count} Nutzern", + "gateway":"auf %{smart_count} Gateway |||| auf %{smart_count} Gateways", + "lastUpdate":"Letzte Aktualisierung", + "nodeNew":"Knoten ist neu", + "nodeOnline":"Knoten ist online", + "nodeOffline":"Knoten ist offline", + "aboutInfo":"

Über Meshviewer

Mit Doppelklick kann man in die Karte hinein zoomen und Shift+Doppelklick heraus zoomen.

", + "actual":"Aktuell", + "stats":"Statistiken", + "about":"Über" + }, + "momentjs":{ + "calendar":{ + "sameDay":"[heute um] LT [Uhr]", + "nextDay":"[morgen um] LT [Uhr]", + "nextWeek":"dddd [um] LT [Uhr]", + "lastDay":"[gestern um] LT [Uhr]", + "lastWeek":"[letzten] dddd [um] LT [Uhr]", + "sameElse":"L" + }, + "relativeTime":{ + "future":"in %s", + "past":"vor %s", + "s":"ein paar Sekunden", + "m":"einer Minute", + "mm":"%d Minuten", + "h":"einer Stunde", + "hh":"%d Stunden", + "d":"einem Tag", + "dd":"%d Tagen", + "M":"einem Monat", + "MM":"%d Monate", + "y":"einem Jahr", + "yy":"%d Jahre" + } + }, + "yes":"ja", + "no":"nein", + "unknown":"unbekannt", + "none":"keine" +} diff --git a/locale/en.json b/locale/en.json new file mode 100644 index 0000000..1b4ec56 --- /dev/null +++ b/locale/en.json @@ -0,0 +1,83 @@ +{ + "node": { + "all": "All nodes", + "nodes": "Nodes", + "uptime": "Uptime", + "links": "Links", + "clients": "Clients", + "distance": "Distance", + "tq": "TQ", + "lastOnline": "online, last message %{time} (%{date})", + "lastOffline": "offline, last message %{time} (%{date})", + "activated": "activated (%{branch})", + "deactivated": "deactivated", + "status": "Status", + "firmware": "Firmware version", + "hardware": "Hardware model", + "visible": "Visible on the map", + "update": "Auto update", + "site": "Site", + "gateway": "Gateway", + "coordinates": "Coordinates", + "contact": "Contact", + "primaryMac": "Primary MAC", + "id": "Node ID", + "firstSeen": "First seen", + "systemLoad": "Load average", + "ram": "Memory usage", + "ipAddresses": "IP addresses", + "selectedGateway": "Selected gateway", + "link": "Link |||| Links", + "node": "Node |||| Nodes", + "new": "New nodes", + "missing": "Disappeared nodes" + }, + "location": { + "location": "Location", + "latitude": "Latitude", + "longitude": "Longitude", + "copy": "Copy" + }, + "sidebar": { + "nodes": "%{total} nodes, including %{online} nodes online", + "clients": "with %{smart_count} client |||| with %{smart_count} clients", + "gateway": "on %{smart_count} gateway |||| on %{smart_count} gateways", + "lastUpdate": "Last update", + "nodeNew": "Node is new", + "nodeOnline": "Node is online", + "nodeOffline": "Node is offline", + "aboutInfo": "

About Meshviewer

You can zoom in with double-click and zoom out with shift+double-click

", + "actual": "Actual", + "stats": "Statistics", + "about": "About" + }, + "momentjs": { + "calendar": { + "sameDay": "[Today at] LT", + "nextDay": "[Tomorrow at] LT", + "nextWeek": "dddd [at] LT", + "lastDay": "[Yesterday at] LT", + "lastWeek": "[Last] dddd [at] LT", + "sameElse": "L" + }, + "relativeTime": { + "future": "in %s", + "past": "%s ago", + "s": "a few seconds", + "m": "a minute", + "mm": "%d minutes", + "h": "an hour", + "hh": "%d hours", + "d": "a day", + "dd": "%d days", + "M": "a month", + "MM": "%d months", + "y": "a year", + "yy": "%d years" + } + }, + "yes": "yes", + "no": "no", + "unknown": "unknown", + "none": "none" +} diff --git a/package.json b/package.json index bf68067..f72c79d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "grunt-eslint": "^19.0.0", "grunt-inline": "^0.3.6", "grunt-inline-data": "git://github.com/xiaokaike/grunt-inline-data.git#2eeb08f", + "grunt-json-minify": "^1.1.0", "grunt-postcss": "^0.8.0", "grunt-sass": "^2.0.0", "grunt-sass-lint": "^0.2.2" @@ -48,6 +49,7 @@ "leaflet": "https://github.com/davojta/Leaflet.git#stable_0_7_7_1_release", "leaflet-label": "^0.2.1-0", "moment": "^2.17.1", + "node-polyglot": "airbnb/polyglot.js", "promise-polyfill": "^6.0.2", "rbush": "1.4.3", "requirejs": "^2.3.2", diff --git a/tasks/build.js b/tasks/build.js index ec0f4de..97ea336 100644 --- a/tasks/build.js +++ b/tasks/build.js @@ -33,6 +33,12 @@ module.exports = function exports(grunt) { expand: true, dest: 'build/', cwd: 'assets/' + }, + locale: { + src: ['locale/*'], + expand: true, + dest: 'build/', + cwd: '.' } }, sass: { @@ -123,6 +129,11 @@ module.exports = function exports(grunt) { } } }, + 'json-minify': { + build: { + files: 'build/locale/*.json' + } + }, cachebreaker: { default: { options: { @@ -142,5 +153,6 @@ module.exports = function exports(grunt) { grunt.loadNpmTasks('grunt-inline'); grunt.loadNpmTasks('grunt-inline-data'); grunt.loadNpmTasks('grunt-contrib-htmlmin'); + grunt.loadNpmTasks('grunt-json-minify'); grunt.loadNpmTasks('grunt-cache-breaker'); }; diff --git a/tasks/development.js b/tasks/development.js index 24dc74c..e8a312e 100644 --- a/tasks/development.js +++ b/tasks/development.js @@ -24,12 +24,12 @@ module.exports = function exports(grunt) { }, watch: { html: { - files: ['html/index.html', 'config.json'], - tasks: ['copy', 'inlinedata', 'htmlmin'] + files: ['html/index.html', 'config.json', 'locale/*.json'], + tasks: ['copy', 'inlinedata', 'htmlmin', 'json-minify'] }, sass: { files: ['scss/**/*.scss'], - tasks: ['sasslint', 'sass', 'postcss'] + tasks: ['sasslint', 'sass', 'postcss'] }, js: { files: ['app.js', 'lib/**/*.js'], diff --git a/yarn.lock b/yarn.lock index 6fc33d0..e423ab1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -926,7 +926,7 @@ error@^4.3.0: string-template "~0.2.0" xtend "~4.0.0" -es-abstract@^1.7.0: +es-abstract@^1.5.0, es-abstract@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" dependencies: @@ -1286,6 +1286,12 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +for-each@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" + dependencies: + is-function "~1.0.0" + for-in@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" @@ -1362,7 +1368,7 @@ fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.1.0: +function-bind@^1.0.2, function-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" @@ -1583,6 +1589,10 @@ grunt-inline@^0.3.6: datauri "~0.2.0" uglify-js "2.4.1" +grunt-json-minify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/grunt-json-minify/-/grunt-json-minify-1.1.0.tgz#a20b9a3016d1e8fed0c79560c77b94701f64f6fb" + grunt-known-options@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.0.tgz#a4274eeb32fa765da5a7a3b1712617ce3b144149" @@ -1704,6 +1714,12 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -1923,6 +1939,10 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-function@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -2473,6 +2493,15 @@ node-gyp@^3.3.1: tar "^2.0.0" which "1" +node-polyglot@airbnb/polyglot.js: + version "2.2.2" + resolved "https://codeload.github.com/airbnb/polyglot.js/tar.gz/eeae4dadf7c0f57bf3266e5370754765ec561037" + dependencies: + for-each "^0.3.2" + has "^1.0.1" + string.prototype.trim "^1.1.2" + warning "^3.0.0" + node-pre-gyp@^0.6.29: version "0.6.32" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz#fc452b376e7319b3d255f5f34853ef6fd8fe1fd5" @@ -3314,6 +3343,14 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string.prototype.trim@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.0" + function-bind "^1.0.2" + string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -3350,11 +3387,11 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-json-comments@1.0.2: +strip-json-comments@1.0.2, strip-json-comments@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.2.tgz#5a48ab96023dbac1b7b8d0ffabf6f63f1677be9f" -strip-json-comments@~1.0.1, strip-json-comments@~1.0.4: +strip-json-comments@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" @@ -3593,6 +3630,12 @@ vlq@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.1.tgz#14439d711891e682535467f8587c5630e4222a6c" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + websocket-driver@>=0.5.1: version "0.6.5" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"