From 1fed01fe431e6671b44faf07f01402c5310cbb9b Mon Sep 17 00:00:00 2001 From: barisusakli Date: Mon, 19 Dec 2016 21:40:09 +0300 Subject: [PATCH 01/28] ability to filter search by tags --- package.json | 4 +-- public/language/en-GB/search.json | 1 + public/src/client/search.js | 21 ++++++++++++--- public/src/modules/autocomplete.js | 35 +++++++++++++++++++++++++ public/src/modules/search.js | 4 +++ src/controllers/search.js | 1 + src/meta/dependencies.js | 1 + src/search.js | 42 +++++++++++++++++++++++++----- src/topics/tags.js | 7 +++++ test/search.js | 16 ++++++++++-- 10 files changed, 119 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 83c62b7e9e..6bfa27004b 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "start": "node loader.js", "lint": "eslint --cache .", "pretest": "npm run lint", - "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R spec", - "coveralls": "istanbul cover _mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" + "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R dot", + "coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, "dependencies": { "async": "~1.5.0", diff --git a/public/language/en-GB/search.json b/public/language/en-GB/search.json index fde9db35f6..98c1afcea2 100644 --- a/public/language/en-GB/search.json +++ b/public/language/en-GB/search.json @@ -8,6 +8,7 @@ "posted-by": "Posted by", "in-categories": "In Categories", "search-child-categories": "Search child categories", + "has-tags": "Has tags", "reply-count": "Reply Count", "at-least": "At least", "at-most": "At most", diff --git a/public/src/client/search.js b/public/src/client/search.js index 20c85c13c2..48cf358c0c 100644 --- a/public/src/client/search.js +++ b/public/src/client/search.js @@ -12,8 +12,6 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc var searchIn = $('#search-in'); - fillOutForm(); - searchIn.on('change', function () { updateFormItemVisiblity(searchIn.val()); }); @@ -31,6 +29,8 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc handleSavePreferences(); enableAutoComplete(); + + fillOutForm(); }; function getSearchData() { @@ -43,6 +43,7 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc searchData.by = form.find('#posted-by-user').val(); searchData.categories = form.find('#posted-in-categories').val(); searchData.searchChildren = form.find('#search-children').is(':checked'); + searchData.hasTags = form.find('#has-tags').tagsinput('items'); searchData.replies = form.find('#reply-count').val(); searchData.repliesFilter = form.find('#reply-count-filter').val(); searchData.timeFilter = form.find('#post-time-filter').val(); @@ -79,7 +80,6 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc $('#posted-by-user').val(formData.by); } - if (formData.categories) { $('#posted-in-categories').val(formData.categories); } @@ -88,6 +88,13 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc $('#search-children').prop('checked', true); } + if (formData.hasTags) { + formData.hasTags = Array.isArray(formData.hasTags) ? formData.hasTags : [formData.hasTags]; + formData.hasTags.forEach(function (tag) { + $('#has-tags').tagsinput('add', tag); + }); + } + if (formData.replies) { $('#reply-count').val(formData.replies); $('#reply-count-filter').val(formData.repliesFilter); @@ -157,6 +164,14 @@ define('forum/search', ['search', 'autocomplete'], function (searchModule, autoc function enableAutoComplete() { autocomplete.user($('#posted-by-user')); + + var tagEl = $('#has-tags'); + tagEl.tagsinput({ + confirmKeys: [13, 44], + trimValue: true + }); + + autocomplete.tag($('#has-tags').siblings('.bootstrap-tagsinput').find('input')); } return Search; diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index c20f804f38..ecaf563d10 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -75,5 +75,40 @@ define('autocomplete', function () { }); }; + module.tag = function (input, onselect) { + app.loadJQueryUI(function () { + input.autocomplete({ + delay: 100, + open: function () { + $(this).autocomplete('widget').css('z-index', 20000); + }, + select: function (event, ui) { + onselect = onselect || function () {}; + var e = jQuery.Event('keypress'); + e.which = 13; + e.keyCode = 13; + setTimeout(function () { + input.trigger(e); + }, 100); + onselect(event, ui); + }, + source: function (request, response) { + socket.emit('topics.autocompleteTags', { + query: request.term, + cid: ajaxify.data.cid || 0 + }, function (err, tags) { + if (err) { + return app.alertError(err.message); + } + if (tags) { + response(tags); + } + $('.ui-autocomplete a').attr('data-ajaxify', 'false'); + }); + } + }); + }); + }; + return module; }); diff --git a/public/src/modules/search.js b/public/src/modules/search.js index 4319de5617..990c9dec97 100644 --- a/public/src/modules/search.js +++ b/public/src/modules/search.js @@ -53,6 +53,10 @@ define('search', ['navigator', 'translator'], function (nav, translator) { } } + if (data.hasTags && data.hasTags.length) { + query.hasTags = data.hasTags; + } + if (parseInt(data.replies, 10) > 0) { query.replies = data.replies; query.repliesFilter = data.repliesFilter || 'atleast'; diff --git a/src/controllers/search.js b/src/controllers/search.js index 5967cfc88e..2261e4f3b7 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -33,6 +33,7 @@ searchController.search = function (req, res, next) { postedBy: req.query.by, categories: req.query.categories, searchChildren: req.query.searchChildren, + hasTags: req.query.hasTags, replies: req.query.replies, repliesFilter: req.query.repliesFilter, timeRange: req.query.timeRange, diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js index 6c5be29e99..5ec596ae5b 100644 --- a/src/meta/dependencies.js +++ b/src/meta/dependencies.js @@ -41,6 +41,7 @@ module.exports = function (Meta) { next(true); } } catch(e) { + console.log(e); process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n'); depsMissing = true; next(true); diff --git a/src/search.js b/src/search.js index 7f03634625..d77b1f58f0 100644 --- a/src/search.js +++ b/src/search.js @@ -126,6 +126,7 @@ function filterAndSort(pids, data, callback) { posts = filterByPostcount(posts, data.replies, data.repliesFilter); posts = filterByTimerange(posts, data.timeRange, data.timeFilter); + posts = filterByTags(posts, data.hasTags); sortPosts(posts, data); @@ -166,6 +167,7 @@ function getMatchedPosts(pids, data, callback) { var keys = pids.map(function (pid) { return 'post:' + pid; }); + db.getObjectsFields(keys, postFields, next); }, function (_posts, next) { @@ -185,7 +187,7 @@ function getMatchedPosts(pids, data, callback) { } }, topics: function (next) { - var topics; + var topicsData; async.waterfall([ function (next) { var topicKeys = posts.map(function (post) { @@ -194,12 +196,12 @@ function getMatchedPosts(pids, data, callback) { db.getObjectsFields(topicKeys, topicFields, next); }, function (_topics, next) { - topics = _topics; + topicsData = _topics; async.parallel({ teasers: function (next) { if (topicFields.indexOf('teaserPid') !== -1) { - var teaserKeys = topics.map(function (topic) { + var teaserKeys = topicsData.map(function (topic) { return 'post:' + topic.teaserPid; }); db.getObjectsFields(teaserKeys, ['timestamp'], next); @@ -211,10 +213,20 @@ function getMatchedPosts(pids, data, callback) { if (!categoryFields.length) { return next(); } - var cids = topics.map(function (topic) { + var cids = topicsData.map(function (topic) { return 'category:' + topic.cid; }); db.getObjectsFields(cids, categoryFields, next); + }, + tags: function (next) { + if (data.hasTags && data.hasTags.length) { + var tids = posts.map(function (post) { + return post && post.tid; + }); + topics.getTopicsTags(tids, next); + } else { + setImmediate(next); + } } }, next); } @@ -223,16 +235,19 @@ function getMatchedPosts(pids, data, callback) { return next(err); } - topics.forEach(function (topic, index) { + topicsData.forEach(function (topic, index) { if (topic && results.categories && results.categories[index]) { topic.category = results.categories[index]; } if (topic && results.teasers && results.teasers[index]) { topic.teaser = results.teasers[index]; } + if (topic && results.tags && results.tags[index]) { + topic.tags = results.tags[index]; + } }); - next(null, topics); + next(null, topicsData); }); } }, next); @@ -297,6 +312,21 @@ function filterByTimerange(posts, timeRange, timeFilter) { return posts; } +function filterByTags(posts, hasTags) { + if (hasTags && hasTags.length) { + posts = posts.filter(function (post) { + var hasAllTags = false; + if (post && post.topic && post.topic.tags && post.topic.tags.length) { + hasAllTags = hasTags.every(function (tag) { + return post.topic.tags.indexOf(tag) !== -1; + }); + } + return hasAllTags; + }); + } + return posts; +} + function sortPosts(posts, data) { if (!posts.length || !data.sortBy) { return; diff --git a/src/topics/tags.js b/src/topics/tags.js index db1be50fbe..a360de290e 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -203,6 +203,13 @@ module.exports = function (Topics) { db.getSetMembers('topic:' + tid + ':tags', callback); }; + Topics.getTopicsTags = function (tids, callback) { + var keys = tids.map(function (tid) { + return 'topic:' + tid + ':tags'; + }); + db.getSetsMembers(keys, callback); + }; + Topics.getTopicTagsObjects = function (tid, callback) { Topics.getTopicsTagsObjects([tid], function (err, data) { callback(err, Array.isArray(data) && data.length ? data[0] : []); diff --git a/test/search.js b/test/search.js index b88dcd22fd..7c619d0b6c 100644 --- a/test/search.js +++ b/test/search.js @@ -61,7 +61,7 @@ describe('Search', function () { cid: cid1, title: 'nodebb mongodb bugs', content: 'avocado cucumber apple orange fox', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin'] + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'] }, next); }, function (results, next) { @@ -73,7 +73,7 @@ describe('Search', function () { cid: cid2, title: 'java mongodb redis', content: 'avocado cucumber carrot armadillo', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin'] + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'] }, next); }, function (results, next) { @@ -155,6 +155,18 @@ describe('Search', function () { }); }); + it('should search with tags filter', function (done) { + search.search({ + query: 'mongodb', + searchIn: 'titles', + hasTags: ['nodebb', 'javascript'] + }, function (err, data) { + assert.ifError(err); + assert.equal(data.posts[0].tid, topic2Data.tid); + done(); + }); + }); + after(function (done) { db.emptydb(done); }); From 9ace5c6e17ec0478e3c31074d490faebd2deeef0 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Mon, 19 Dec 2016 21:46:28 +0300 Subject: [PATCH 02/28] moved next() of out try/catch --- src/meta/dependencies.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js index 5ec596ae5b..3f892c37ab 100644 --- a/src/meta/dependencies.js +++ b/src/meta/dependencies.js @@ -31,19 +31,19 @@ module.exports = function (Meta) { try { pkgData = JSON.parse(pkgData); - var ok = !semver.validRange(pkg.dependencies[module]) || semver.satisfies(pkgData.version, pkg.dependencies[module]); - - if (ok || (pkgData._resolved && pkgData._resolved.indexOf('//github.com') !== -1)) { - next(true); - } else { - process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n'); - depsOutdated = true; - next(true); - } } catch(e) { - console.log(e); process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n'); depsMissing = true; + return next(true); + } + + var ok = !semver.validRange(pkg.dependencies[module]) || semver.satisfies(pkgData.version, pkg.dependencies[module]); + + if (ok || (pkgData._resolved && pkgData._resolved.indexOf('//github.com') !== -1)) { + next(true); + } else { + process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n'); + depsOutdated = true; next(true); } }); From 70f71888f1aaf7d88f58466d29265e43746e2bb0 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Mon, 19 Dec 2016 21:53:19 +0300 Subject: [PATCH 03/28] up themes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6bfa27004b..4d2b1b5994 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "nodebb-plugin-spam-be-gone": "0.4.10", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "3.0.15", - "nodebb-theme-persona": "4.1.88", - "nodebb-theme-vanilla": "5.1.56", + "nodebb-theme-persona": "4.1.89", + "nodebb-theme-vanilla": "5.1.57", "nodebb-widget-essentials": "2.0.13", "nodemailer": "2.6.4", "nodemailer-sendmail-transport": "1.0.0", From ecdd57dda2b8b0ae53bb3c3c0ff977625a9e7a0c Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Mon, 19 Dec 2016 19:17:56 +0000 Subject: [PATCH 04/28] Incremented version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d2b1b5994..f6b316bf31 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "1.4.0", + "version": "1.4.1", "homepage": "http://www.nodebb.org", "repository": { "type": "git", From 893170213606f8bbc591537561e9ede9924b9674 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 1 Dec 2016 19:16:55 -0700 Subject: [PATCH 05/28] `admin/advanced` translations --- .../language/en-GB/admin/advanced/cache.json | 11 ++++ .../en-GB/admin/advanced/database.json | 35 +++++++++++ .../language/en-GB/admin/advanced/errors.json | 12 ++++ .../language/en-GB/admin/advanced/events.json | 6 ++ .../language/en-GB/admin/advanced/logs.json | 6 ++ src/views/admin/advanced/cache.tpl | 22 +++---- src/views/admin/advanced/database.tpl | 60 +++++++++---------- src/views/admin/advanced/errors.tpl | 30 +++++++--- src/views/admin/advanced/events.tpl | 10 ++-- src/views/admin/advanced/logs.tpl | 12 ++-- 10 files changed, 146 insertions(+), 58 deletions(-) create mode 100644 public/language/en-GB/admin/advanced/cache.json create mode 100644 public/language/en-GB/admin/advanced/database.json create mode 100644 public/language/en-GB/admin/advanced/errors.json create mode 100644 public/language/en-GB/admin/advanced/events.json create mode 100644 public/language/en-GB/admin/advanced/logs.json diff --git a/public/language/en-GB/admin/advanced/cache.json b/public/language/en-GB/admin/advanced/cache.json new file mode 100644 index 0000000000..5a954f1232 --- /dev/null +++ b/public/language/en-GB/admin/advanced/cache.json @@ -0,0 +1,11 @@ +{ + "post-cache": "Post Cache", + "posts-in-cache": "Posts in Cache", + "average-post-size": "Average Post Size", + "length-to-max": "Length / Max", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache", + "control-panel": "Control Panel", + "update-settings": "Update Cache Settings" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/database.json b/public/language/en-GB/admin/advanced/database.json new file mode 100644 index 0000000000..f7db6220ee --- /dev/null +++ b/public/language/en-GB/admin/advanced/database.json @@ -0,0 +1,35 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.raw-info": "MongoDB Raw Info", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/errors.json b/public/language/en-GB/admin/advanced/errors.json new file mode 100644 index 0000000000..dc9425b4c4 --- /dev/null +++ b/public/language/en-GB/admin/advanced/errors.json @@ -0,0 +1,12 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! There are no routes that were not found." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/events.json b/public/language/en-GB/admin/advanced/events.json new file mode 100644 index 0000000000..766eb5e951 --- /dev/null +++ b/public/language/en-GB/admin/advanced/events.json @@ -0,0 +1,6 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/logs.json b/public/language/en-GB/admin/advanced/logs.json new file mode 100644 index 0000000000..7426b14a36 --- /dev/null +++ b/public/language/en-GB/admin/advanced/logs.json @@ -0,0 +1,6 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs" +} \ No newline at end of file diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl index bd01bf99e0..70f82ab5db 100644 --- a/src/views/admin/advanced/cache.tpl +++ b/src/views/admin/advanced/cache.tpl @@ -2,25 +2,25 @@
-
Post Cache
+
[[admin/advanced/cache:post-cache]]
-
+
{postCache.itemCount}
-
+
{postCache.avgPostSize}
-
+
{postCache.length} / {postCache.max}
- {postCache.percentFull}% Full + [[admin/advanced/cache:percent-full, {postCache.percentFull}]]
- +
@@ -30,15 +30,15 @@
Group Cache
-
+
{groupCache.itemCount}
-
+
{groupCache.length} / {groupCache.max}
- {groupCache.percentFull}% Full + [[admin/advanced/cache:percent-full, {groupCache.percentFull}]]
@@ -51,9 +51,9 @@
-
Control Panel
+
[[admin/advanced/cache:control-panel]]
- +
diff --git a/src/views/admin/advanced/database.tpl b/src/views/admin/advanced/database.tpl index 4ebd277f55..0773a27adc 100644 --- a/src/views/admin/advanced/database.tpl +++ b/src/views/admin/advanced/database.tpl @@ -2,27 +2,27 @@
-
Mongo
+
[[admin/advanced/database:mongo]]
- MongoDB Version {mongo.version}
+ [[admin/advanced/database:mongo.version]] {mongo.version}

- Uptime in Seconds {mongo.uptime}
- Storage Engine {mongo.storageEngine}
- Collections {mongo.collections}
- Objects {mongo.objects}
- Avg. Object Size {mongo.avgObjSize} b
+ [[admin/advanced/database:uptime-seconds]] {mongo.uptime}
+ [[admin/advanced/database:mongo.storage-engine]] {mongo.storageEngine}
+ [[admin/advanced/database:mongo.collections]] {mongo.collections}
+ [[admin/advanced/database:mongo.objects]] {mongo.objects}
+ [[admin/advanced/database:mongo.avg-object-size]] [[admin/advanced/database:x-b, {mongo.avgObjSize}]]

- Data Size {mongo.dataSize} mb
- Storage Size {mongo.storageSize} mb
- Index Size {mongo.indexSize} mb
+ [[admin/advanced/database:mongo.data-size]] [[admin/advanced/database:x-mb, {mongo.dataSize}]]
+ [[admin/advanced/database:mongo.storage-size]] [[admin/advanced/database:x-mb, {mongo.storageSize}]]
+ [[admin/advanced/database:mongo.index-size]] [[admin/advanced/database:x-mb, {mongo.indexSize}]]
- File Size {mongo.fileSize} mb
+ [[admin/advanced/database:mongo.file-size]] [[admin/advanced/database:x-mb, {mongo.fileSize}]]

- Resident Memory {mongo.mem.resident} mb
- Virtual Memory {mongo.mem.virtual} mb
- Mapped Memory {mongo.mem.mapped} mb
+ [[admin/advanced/database:mongo.resident-memory]] [[admin/advanced/database:x-mb, {mongo.mem.resident}]]
+ [[admin/advanced/database:mongo.virtual-memory]] [[admin/advanced/database:x-mb, {mongo.mem.virtual]]
+ [[admin/advanced/database:mongo.mapped-memory]] [[admin/advanced/database:x-mb, {mongo.mem.mapped}]]
@@ -30,28 +30,28 @@
-
Redis
+
[[admin/advanced/database:redis]]
- Redis Version {redis.redis_version}
+ [[admin/advanced/database:redis.version]] {redis.redis_version}

- Uptime in Seconds {redis.uptime_in_seconds}
- Uptime in Days {redis.uptime_in_days}
+ [[admin/advanced/database:uptime-seconds]] {redis.uptime_in_seconds}
+ [[admin/advanced/database:uptime-days]] {redis.uptime_in_days}

- Connected Clients {redis.connected_clients}
- Connected Slaves {redis.connected_slaves}
- Blocked Clients {redis.blocked_clients}
+ [[admin/advanced/database:redis.connected-clients]] {redis.connected_clients}
+ [[admin/advanced/database:redis.connected-slaves]] {redis.connected_slaves}
+ [[admin/advanced/database:redis.blocked-clients]] {redis.blocked_clients}

- Used Memory {redis.used_memory_human}
- Memory Fragmentation Ratio {redis.mem_fragmentation_ratio}
+ [[admin/advanced/database:redis.used-memory]] {redis.used_memory_human}
+ [[admin/advanced/database:redis.memory-frag-ratio]] {redis.mem_fragmentation_ratio}

- Total Connections Received {redis.total_connections_received}
- Total Commands Processed {redis.total_commands_processed}
- Instantaneous Ops. Per Second {redis.instantaneous_ops_per_sec}
+ [[admin/advanced/database:redis.total-connections-recieved]] {redis.total_connections_received}
+ [[admin/advanced/database:redis.total-commands-processed]] {redis.total_commands_processed}
+ [[admin/advanced/database:redis.iops]] {redis.instantaneous_ops_per_sec}

- Keyspace Hits {redis.keyspace_hits}
- Keyspace Misses {redis.keyspace_misses}
+ [[admin/advanced/database:redis.keyspace-hits]] {redis.keyspace_hits}
+ [[admin/advanced/database:redis.keyspace-misses]] {redis.keyspace_misses}
@@ -60,7 +60,7 @@
-

MongoDB Raw Info

+

[[admin/advanced/database:mongo.raw-info]]

@@ -74,7 +74,7 @@
-

Redis Raw Info

+

[[admin/advanced/database:redis.raw-info]]

diff --git a/src/views/admin/advanced/errors.tpl b/src/views/admin/advanced/errors.tpl index d3c23e78b7..f5f0838c31 100644 --- a/src/views/admin/advanced/errors.tpl +++ b/src/views/admin/advanced/errors.tpl @@ -6,7 +6,10 @@
- +
@@ -14,18 +17,25 @@
- +
-
Manage Error Log
+
[[admin/advanced/errors:manage-error-log]]
- Export Error Log (CSV) - + + [[admin/advanced/errors:export-error-log]] + +
@@ -35,12 +45,14 @@
-
404 Not Found
+
+ [[admin/advanced/errors:error.404]] +
- - + + @@ -53,7 +65,7 @@ diff --git a/src/views/admin/advanced/events.tpl b/src/views/admin/advanced/events.tpl index f1c0973501..b80f56ed6e 100644 --- a/src/views/admin/advanced/events.tpl +++ b/src/views/admin/advanced/events.tpl @@ -1,10 +1,10 @@
-
Events
+
[[admin/advanced/events:events]]
-
There are no events
+
[[admin/advanced/events:no-events]]
@@ -30,9 +30,11 @@
-
Events Control Panel
+
- +
diff --git a/src/views/admin/advanced/logs.tpl b/src/views/admin/advanced/logs.tpl index 9eda8fc383..a8f8a501a3 100644 --- a/src/views/admin/advanced/logs.tpl +++ b/src/views/admin/advanced/logs.tpl @@ -1,7 +1,7 @@
-
Logs
+
[[admin/advanced/logs:logs]]
{data}
@@ -9,10 +9,14 @@
-
Logs Control Panel
+
[[admin/advanced/logs:control-panel]]
- - + +
From 6c1b852d480609efd7b3780a44b6133cdaaca085 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 1 Dec 2016 19:27:52 -0700 Subject: [PATCH 06/28] `admin/appearance` translations --- .../language/en-GB/admin/appearance/customize.json | 9 +++++++++ src/views/admin/appearance/customise.tpl | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 public/language/en-GB/admin/appearance/customize.json diff --git a/public/language/en-GB/admin/appearance/customize.json b/public/language/en-GB/admin/appearance/customize.json new file mode 100644 index 0000000000..767d443e29 --- /dev/null +++ b/public/language/en-GB/admin/appearance/customize.json @@ -0,0 +1,9 @@ +{ + "custom-css": "Custom CSS", + "custom-css.description": "Enter your own CSS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. JavaScript, Meta Tags, etc.), which will be appended to the <head> section of your forum's markup.", + "custom-header.enable": "Enable Custom Header" +} \ No newline at end of file diff --git a/src/views/admin/appearance/customise.tpl b/src/views/admin/appearance/customise.tpl index b4d54bec13..297c5d3f88 100644 --- a/src/views/admin/appearance/customise.tpl +++ b/src/views/admin/appearance/customise.tpl @@ -1,13 +1,13 @@

- Enter your own CSS declarations here, which will be applied after all other styles. + [[admin/appearance/customize:custom-css.description]]

@@ -17,14 +17,14 @@

- Enter custom HTML here (ex. JavaScript, Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. + [[admin/appearance/customize:custom-header.description]]

@@ -35,7 +35,7 @@
From 50aed01c57de5748e063df6c2eb0f177608f2559 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 1 Dec 2016 20:08:30 -0700 Subject: [PATCH 07/28] `admin/development` translations --- .../en-GB/admin/development/info.json | 16 +++++++++++++ .../en-GB/admin/development/logger.json | 12 ++++++++++ src/views/admin/development/info.tpl | 24 +++++++++++-------- src/views/admin/development/logger.tpl | 18 +++++++------- 4 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 public/language/en-GB/admin/development/info.json create mode 100644 public/language/en-GB/admin/development/logger.json diff --git a/public/language/en-GB/admin/development/info.json b/public/language/en-GB/admin/development/info.json new file mode 100644 index 0000000000..b2768ca212 --- /dev/null +++ b/public/language/en-GB/admin/development/info.json @@ -0,0 +1,16 @@ +{ + "you-are-on": "Info - You are on %1:%2", + "host": "host", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "load": "load", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/development/logger.json b/public/language/en-GB/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/en-GB/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/src/views/admin/development/info.tpl b/src/views/admin/development/info.tpl index c4f9ddde1b..50963d4d8c 100644 --- a/src/views/admin/development/info.tpl +++ b/src/views/admin/development/info.tpl @@ -1,20 +1,20 @@
-

Info - You are on {host}:{port}

+

[[admin/development/info:you-are-on, {host}, {port}]]

RouteCount[[admin/advanced/errors:route]][[admin/advanced/errors:count]]
- Hooray! There are no routes that were not found. + [[admin/advanced/errors:no-routes-not-found]]
- - - - - - - + + + + + + + @@ -23,7 +23,11 @@ - + @@ -37,7 +41,7 @@
-

Info

+

[[admin/development/info:info]]

diff --git a/src/views/admin/development/logger.tpl b/src/views/admin/development/logger.tpl index 4a280238b7..7b33625931 100644 --- a/src/views/admin/development/logger.tpl +++ b/src/views/admin/development/logger.tpl @@ -1,33 +1,33 @@
-
Logger Settings
+
[[admin/development/logger:logger-settings]]

- By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals. + [[admin/development/logger:description]]


- Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed. + [[admin/development/logger:explanation]]






- - + +
@@ -36,9 +36,9 @@
-
Logger Control Panel
+
[[admin/development/logger:control-panel]]
- +
From 3cd6a8a94b86be05c8f2633a51b08b2d2db00dab Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 1 Dec 2016 20:56:09 -0700 Subject: [PATCH 08/28] `admin/extend` translations --- .../language/en-GB/admin/extend/plugins.json | 29 +++++++++++++++++ .../language/en-GB/admin/extend/rewards.json | 13 ++++++++ .../language/en-GB/admin/extend/widgets.json | 14 ++++++++ src/views/admin/extend/plugins.tpl | 32 +++++++++---------- src/views/admin/extend/rewards.tpl | 24 +++++++------- src/views/admin/extend/widgets.tpl | 24 +++++++------- .../admin/partials/installed_plugin_item.tpl | 24 ++++++++------ 7 files changed, 111 insertions(+), 49 deletions(-) create mode 100644 public/language/en-GB/admin/extend/plugins.json create mode 100644 public/language/en-GB/admin/extend/rewards.json create mode 100644 public/language/en-GB/admin/extend/widgets.json diff --git a/public/language/en-GB/admin/extend/plugins.json b/public/language/en-GB/admin/extend/plugins.json new file mode 100644 index 0000000000..92d8d4a1b9 --- /dev/null +++ b/public/language/en-GB/admin/extend/plugins.json @@ -0,0 +1,29 @@ +{ + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/extend/rewards.json b/public/language/en-GB/admin/extend/rewards.json new file mode 100644 index 0000000000..74fa0c6436 --- /dev/null +++ b/public/language/en-GB/admin/extend/rewards.json @@ -0,0 +1,13 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + "control-panel": "Rewards Control", + "new-reward": "New Reward" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/extend/widgets.json b/public/language/en-GB/admin/extend/widgets.json new file mode 100644 index 0000000000..88ee38fc7c --- /dev/null +++ b/public/language/en-GB/admin/extend/widgets.json @@ -0,0 +1,14 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the essential widgets plugin in the plugins control panel.", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert" +} \ No newline at end of file diff --git a/src/views/admin/extend/plugins.tpl b/src/views/admin/extend/plugins.tpl index 07c51bcb73..34f93ff424 100644 --- a/src/views/admin/extend/plugins.tpl +++ b/src/views/admin/extend/plugins.tpl @@ -1,12 +1,12 @@
@@ -41,24 +41,24 @@
-
Plugin Search
+
[[admin/extend/plugins:plugin-search]]
-
+
-
Re-order Plugins
+
[[admin/extend/plugins:reorder-plugins]]
- +
-
Interested in writing plugins for NodeBB?
+
[[admin/extend/plugins:dev-interested]]

- Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal. + [[admin/extend/plugins:documentation-info]]

@@ -70,20 +70,20 @@
diff --git a/src/views/admin/extend/rewards.tpl b/src/views/admin/extend/rewards.tpl index 9d2a2fd0f4..b4256a41d3 100644 --- a/src/views/admin/extend/rewards.tpl +++ b/src/views/admin/extend/rewards.tpl @@ -1,14 +1,14 @@
-
Rewards
+
[[admin/extend/rewards:rewards]]
  • -
    +
    -
    +
    -
    +
    - Enter 0 for infinite + [[admin/extend/rewards:zero-infinite]]
    - + - + - +
@@ -66,10 +66,10 @@
-
Rewards Control
+
[[admin/extend/rewards:control-panel]]
- - + +
diff --git a/src/views/admin/extend/widgets.tpl b/src/views/admin/extend/widgets.tpl index 381d03794e..07b8b5c193 100644 --- a/src/views/admin/extend/widgets.tpl +++ b/src/views/admin/extend/widgets.tpl @@ -28,12 +28,12 @@
-
Available Widgets
+
[[admin/extend/widgets:available]]
-

Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.

+

[[admin/extend/widgets:explanation]]

-
No widgets found! Activate the essential widgets plugin in the plugins control panel.
+
[[none-installed, {config.relative_path}/admin/extend/plugins]]

@@ -14,14 +14,14 @@


diff --git a/src/views/admin/general/languages.tpl b/src/views/admin/general/languages.tpl index 1a2f56f185..310d1a366d 100644 --- a/src/views/admin/general/languages.tpl +++ b/src/views/admin/general/languages.tpl @@ -1,16 +1,14 @@
-
Language Settings
+
[[admin/general/languages:language-settings]]

- The default language determines the language settings for all users who - are visiting your forum.
- Individual users can override the default language on their account settings page. + [[admin/general/languages:description]]

- +
- +
- +
- +
- +
- +
- Properties: + [[admin/general/navigation:properties]]
- Installed Plugins Required: + [[admin/general/navigation:installed-plugins-required]]
- + - + - + @@ -108,7 +109,7 @@
-
Available Menu Items
+
[[admin/general/navigation:available-menu-items]]
  • @@ -116,7 +117,7 @@

- Custom Route + [[admin/general/navigation:custom-route]]

@@ -126,7 +127,7 @@

{available.text} {available.route}
- core plugin + [[admin/general/navigation:core]] [[plugin]]

diff --git a/src/views/admin/general/social.tpl b/src/views/admin/general/social.tpl index 5ce9ccfc84..152dcef3fa 100644 --- a/src/views/admin/general/social.tpl +++ b/src/views/admin/general/social.tpl @@ -1,7 +1,7 @@ diff --git a/src/views/admin/general/sounds.tpl b/src/views/admin/general/sounds.tpl index 1154f4ec75..d37447d56e 100644 --- a/src/views/admin/general/sounds.tpl +++ b/src/views/admin/general/sounds.tpl @@ -2,9 +2,9 @@
-
Notifications
+
[[admin/general/sounds:notifications]]
- +
- +
-
Chat Messages
+
[[admin/general/sounds:chat-messages]]
- +
- +
- +
- +
- +
From 0b4c39338e5e668065af21d54f6ce6c00c269086 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 7 Dec 2016 22:30:18 -0700 Subject: [PATCH 11/28] Translation bootbox wrapper - Replaced minfied bootbox file with unminified one since it's minified at build anyways - Removed existing override - Made translator more verbose in dev mode; it now warns about missing translations --- public/src/app.js | 1 - public/src/modules/translator.js | 25 +- public/src/overrides.js | 37 -- public/vendor/bootbox/bootbox.js | 830 +++++++++++++++++++++++++++ public/vendor/bootbox/bootbox.min.js | 7 - public/vendor/bootbox/wrapper.js | 44 ++ src/meta/js.js | 3 +- 7 files changed, 895 insertions(+), 52 deletions(-) create mode 100644 public/vendor/bootbox/bootbox.js delete mode 100644 public/vendor/bootbox/bootbox.min.js create mode 100644 public/vendor/bootbox/wrapper.js diff --git a/public/src/app.js b/public/src/app.js index 7c69d9fb61..fe12a7adfc 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -53,7 +53,6 @@ app.cacheBuster = null; } }); - overrides.overrideBootbox(); createHeaderTooltips(); app.showEmailConfirmWarning(); app.showCookieWarning(); diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 30b67ef3a8..c19c705eaf 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -5,10 +5,14 @@ function loadClient(language, namespace) { return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + encodeURIComponent(namespace))); } + var warn = function () {}; + if (typeof config === 'object' && config.environment === 'development') { + warn = console.warn.bind(console); + } if (typeof define === 'function' && define.amd) { // AMD. Register as a named module define('translator', ['string'], function (string) { - return factory(string, loadClient); + return factory(string, loadClient, warn); }); } else if (typeof module === 'object' && module.exports) { // Node @@ -16,6 +20,13 @@ require('promise-polyfill'); var languages = require('../../../src/languages'); + if (global.env === 'development') { + var winston = require('winston'); + warn = function (a, b, c, d) { + winston.warn(a, b, c, d); + }; + } + function loadServer(language, namespace) { return new Promise(function (resolve, reject) { languages.get(language, namespace, function (err, data) { @@ -28,12 +39,12 @@ }); } - module.exports = factory(require('string'), loadServer); + module.exports = factory(require('string'), loadServer, warn); }()); } else { - window.translator = factory(window.string, loadClient); + window.translator = factory(window.string, loadClient, warn); } -}(function (string, load) { +}(function (string, load, warn) { 'use strict'; var assign = Object.assign || jQuery.extend; function classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } @@ -238,6 +249,7 @@ } if (namespace && !key) { + warn('Missing key in translation token "' + name + '"'); return Promise.resolve('[[' + namespace + ']]'); } @@ -256,6 +268,7 @@ var translatedArgs = result.slice(1); if (!translated) { + warn('Missing translation "' + name + '"'); return key; } var out = translated; @@ -276,7 +289,7 @@ Translator.prototype.getTranslation = function getTranslation(namespace, key) { var translation; if (!namespace) { - console.warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : '')); + warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : '')); translation = Promise.resolve({}); } else { translation = this.translations[namespace] = this.translations[namespace] || this.load(this.lang, namespace); @@ -441,7 +454,7 @@ Translator.create(lang).translate(text).then(function (output) { return cb(output); }).catch(function (err) { - console.error('Translation failed: ' + err.stack); + warn('Translation failed: ' + err.stack); }); }, diff --git a/public/src/overrides.js b/public/src/overrides.js index a5216910d2..7ee354a06d 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -111,43 +111,6 @@ if ('undefined' !== typeof window) { }); }()); - overrides.overrideBootbox = function () { - require(['translator'], function (translator) { - var dialog = bootbox.dialog, - prompt = bootbox.prompt, - confirm = bootbox.confirm; - - function translate(modal) { - var header = modal.find('.modal-header'), - footer = modal.find('.modal-footer'); - translator.translate(header.html(), function (html) { - header.html(html); - }); - translator.translate(footer.html(), function (html) { - footer.html(html); - }); - } - - bootbox.dialog = function () { - var modal = $(dialog.apply(this, arguments)[0]); - translate(modal); - return modal; - }; - - bootbox.prompt = function () { - var modal = $(prompt.apply(this, arguments)[0]); - translate(modal); - return modal; - }; - - bootbox.confirm = function () { - var modal = $(confirm.apply(this, arguments)[0]); - translate(modal); - return modal; - }; - }); - }; - overrides.overrideTimeago = function () { var timeagoFn = $.fn.timeago; if (parseInt(config.timeagoCutoff, 10) === 0) { diff --git a/public/vendor/bootbox/bootbox.js b/public/vendor/bootbox/bootbox.js new file mode 100644 index 0000000000..15a5527f4a --- /dev/null +++ b/public/vendor/bootbox/bootbox.js @@ -0,0 +1,830 @@ +/** + * bootbox.js [v4.4.0] + * + * http://bootboxjs.com/license.txt + */ + +// @see https://github.com/makeusabrew/bootbox/issues/180 +// @see https://github.com/makeusabrew/bootbox/issues/186 +(function (root, factory) { + + "use strict"; + + // ** removed require.js and commonjs module definitions + + // Browser globals (root is window) + root.bootbox = factory(root.jQuery); + +}(this, function init($, undefined) { + + "use strict"; + + // the base DOM structure needed to create a modal + var templates = { + dialog: + "", + header: + "", + footer: + "", + closeButton: + "", + form: + "", + inputs: { + text: + "", + textarea: + "", + email: + "", + select: + "", + checkbox: + "
", + date: + "", + time: + "", + number: + "", + password: + "" + } + }; + + var defaults = { + // default language + locale: "en", + // show backdrop or not. Default to static so user has to interact with dialog + backdrop: "static", + // animate the modal in/out + animate: true, + // additional class string applied to the top level dialog + className: null, + // whether or not to include a close button + closeButton: true, + // show the dialog immediately by default + show: true, + // dialog container + container: "body" + }; + + // our public object; augmented after our private API + var exports = {}; + + /** + * @private + */ + function _t(key) { + var locale = locales[defaults.locale]; + return locale ? locale[key] : locales.en[key]; + } + + function processCallback(e, dialog, callback) { + e.stopPropagation(); + e.preventDefault(); + + // by default we assume a callback will get rid of the dialog, + // although it is given the opportunity to override this + + // so, if the callback can be invoked and it *explicitly returns false* + // then we'll set a flag to keep the dialog active... + var preserveDialog = $.isFunction(callback) && callback.call(dialog, e) === false; + + // ... otherwise we'll bin it + if (!preserveDialog) { + dialog.modal("hide"); + } + } + + function getKeyLength(obj) { + // @TODO defer to Object.keys(x).length if available? + var k, t = 0; + for (k in obj) { + t ++; + } + return t; + } + + function each(collection, iterator) { + var index = 0; + $.each(collection, function(key, value) { + iterator(key, value, index++); + }); + } + + function sanitize(options) { + var buttons; + var total; + + if (typeof options !== "object") { + throw new Error("Please supply an object of options"); + } + + if (!options.message) { + throw new Error("Please specify a message"); + } + + // make sure any supplied options take precedence over defaults + options = $.extend({}, defaults, options); + + if (!options.buttons) { + options.buttons = {}; + } + + buttons = options.buttons; + + total = getKeyLength(buttons); + + each(buttons, function(key, button, index) { + + if ($.isFunction(button)) { + // short form, assume value is our callback. Since button + // isn't an object it isn't a reference either so re-assign it + button = buttons[key] = { + callback: button + }; + } + + // before any further checks make sure by now button is the correct type + if ($.type(button) !== "object") { + throw new Error("button with key " + key + " must be an object"); + } + + if (!button.label) { + // the lack of an explicit label means we'll assume the key is good enough + button.label = key; + } + + if (!button.className) { + if (total <= 2 && index === total-1) { + // always add a primary to the main option in a two-button dialog + button.className = "btn-primary"; + } else { + button.className = "btn-default"; + } + } + }); + + return options; + } + + /** + * map a flexible set of arguments into a single returned object + * if args.length is already one just return it, otherwise + * use the properties argument to map the unnamed args to + * object properties + * so in the latter case: + * mapArguments(["foo", $.noop], ["message", "callback"]) + * -> { message: "foo", callback: $.noop } + */ + function mapArguments(args, properties) { + var argn = args.length; + var options = {}; + + if (argn < 1 || argn > 2) { + throw new Error("Invalid argument length"); + } + + if (argn === 2 || typeof args[0] === "string") { + options[properties[0]] = args[0]; + options[properties[1]] = args[1]; + } else { + options = args[0]; + } + + return options; + } + + /** + * merge a set of default dialog options with user supplied arguments + */ + function mergeArguments(defaults, args, properties) { + return $.extend( + // deep merge + true, + // ensure the target is an empty, unreferenced object + {}, + // the base options object for this type of dialog (often just buttons) + defaults, + // args could be an object or array; if it's an array properties will + // map it to a proper options object + mapArguments( + args, + properties + ) + ); + } + + /** + * this entry-level method makes heavy use of composition to take a simple + * range of inputs and return valid options suitable for passing to bootbox.dialog + */ + function mergeDialogOptions(className, labels, properties, args) { + // build up a base set of dialog properties + var baseOptions = { + className: "bootbox-" + className, + buttons: createLabels.apply(null, labels) + }; + + // ensure the buttons properties generated, *after* merging + // with user args are still valid against the supplied labels + return validateButtons( + // merge the generated base properties with user supplied arguments + mergeArguments( + baseOptions, + args, + // if args.length > 1, properties specify how each arg maps to an object key + properties + ), + labels + ); + } + + /** + * from a given list of arguments return a suitable object of button labels + * all this does is normalise the given labels and translate them where possible + * e.g. "ok", "confirm" -> { ok: "OK, cancel: "Annuleren" } + */ + function createLabels() { + var buttons = {}; + + for (var i = 0, j = arguments.length; i < j; i++) { + var argument = arguments[i]; + var key = argument.toLowerCase(); + var value = argument.toUpperCase(); + + buttons[key] = { + label: _t(value) + }; + } + + return buttons; + } + + function validateButtons(options, buttons) { + var allowedButtons = {}; + each(buttons, function(key, value) { + allowedButtons[value] = true; + }); + + each(options.buttons, function(key) { + if (allowedButtons[key] === undefined) { + throw new Error("button key " + key + " is not allowed (options are " + buttons.join("\n") + ")"); + } + }); + + return options; + } + + exports.alert = function() { + var options; + + options = mergeDialogOptions("alert", ["ok"], ["message", "callback"], arguments); + + if (options.callback && !$.isFunction(options.callback)) { + throw new Error("alert requires callback property to be a function when provided"); + } + + /** + * overrides + */ + options.buttons.ok.callback = options.onEscape = function() { + if ($.isFunction(options.callback)) { + return options.callback.call(this); + } + return true; + }; + + return exports.dialog(options); + }; + + exports.confirm = function() { + var options; + + options = mergeDialogOptions("confirm", ["cancel", "confirm"], ["message", "callback"], arguments); + + /** + * overrides; undo anything the user tried to set they shouldn't have + */ + options.buttons.cancel.callback = options.onEscape = function() { + return options.callback.call(this, false); + }; + + options.buttons.confirm.callback = function() { + return options.callback.call(this, true); + }; + + // confirm specific validation + if (!$.isFunction(options.callback)) { + throw new Error("confirm requires a callback"); + } + + return exports.dialog(options); + }; + + exports.prompt = function() { + var options; + var defaults; + var dialog; + var form; + var input; + var shouldShow; + var inputOptions; + + // we have to create our form first otherwise + // its value is undefined when gearing up our options + // @TODO this could be solved by allowing message to + // be a function instead... + form = $(templates.form); + + // prompt defaults are more complex than others in that + // users can override more defaults + // @TODO I don't like that prompt has to do a lot of heavy + // lifting which mergeDialogOptions can *almost* support already + // just because of 'value' and 'inputType' - can we refactor? + defaults = { + className: "bootbox-prompt", + buttons: createLabels("cancel", "confirm"), + value: "", + inputType: "text" + }; + + options = validateButtons( + mergeArguments(defaults, arguments, ["title", "callback"]), + ["cancel", "confirm"] + ); + + // capture the user's show value; we always set this to false before + // spawning the dialog to give us a chance to attach some handlers to + // it, but we need to make sure we respect a preference not to show it + shouldShow = (options.show === undefined) ? true : options.show; + + /** + * overrides; undo anything the user tried to set they shouldn't have + */ + options.message = form; + + options.buttons.cancel.callback = options.onEscape = function() { + return options.callback.call(this, null); + }; + + options.buttons.confirm.callback = function() { + var value; + + switch (options.inputType) { + case "text": + case "textarea": + case "email": + case "select": + case "date": + case "time": + case "number": + case "password": + value = input.val(); + break; + + case "checkbox": + var checkedItems = input.find("input:checked"); + + // we assume that checkboxes are always multiple, + // hence we default to an empty array + value = []; + + each(checkedItems, function(_, item) { + value.push($(item).val()); + }); + break; + } + + return options.callback.call(this, value); + }; + + options.show = false; + + // prompt specific validation + if (!options.title) { + throw new Error("prompt requires a title"); + } + + if (!$.isFunction(options.callback)) { + throw new Error("prompt requires a callback"); + } + + if (!templates.inputs[options.inputType]) { + throw new Error("invalid prompt type"); + } + + // create the input based on the supplied type + input = $(templates.inputs[options.inputType]); + + switch (options.inputType) { + case "text": + case "textarea": + case "email": + case "date": + case "time": + case "number": + case "password": + input.val(options.value); + break; + + case "select": + var groups = {}; + inputOptions = options.inputOptions || []; + + if (!$.isArray(inputOptions)) { + throw new Error("Please pass an array of input options"); + } + + if (!inputOptions.length) { + throw new Error("prompt with select requires options"); + } + + each(inputOptions, function(_, option) { + + // assume the element to attach to is the input... + var elem = input; + + if (option.value === undefined || option.text === undefined) { + throw new Error("given options in wrong format"); + } + + // ... but override that element if this option sits in a group + + if (option.group) { + // initialise group if necessary + if (!groups[option.group]) { + groups[option.group] = $("").attr("label", option.group); + } + + elem = groups[option.group]; + } + + elem.append(""); + }); + + each(groups, function(_, group) { + input.append(group); + }); + + // safe to set a select's value as per a normal input + input.val(options.value); + break; + + case "checkbox": + var values = $.isArray(options.value) ? options.value : [options.value]; + inputOptions = options.inputOptions || []; + + if (!inputOptions.length) { + throw new Error("prompt with checkbox requires options"); + } + + if (!inputOptions[0].value || !inputOptions[0].text) { + throw new Error("given options in wrong format"); + } + + // checkboxes have to nest within a containing element, so + // they break the rules a bit and we end up re-assigning + // our 'input' element to this container instead + input = $("
"); + + each(inputOptions, function(_, option) { + var checkbox = $(templates.inputs[options.inputType]); + + checkbox.find("input").attr("value", option.value); + checkbox.find("label").append(option.text); + + // we've ensured values is an array so we can always iterate over it + each(values, function(_, value) { + if (value === option.value) { + checkbox.find("input").prop("checked", true); + } + }); + + input.append(checkbox); + }); + break; + } + + // @TODO provide an attributes option instead + // and simply map that as keys: vals + if (options.placeholder) { + input.attr("placeholder", options.placeholder); + } + + if (options.pattern) { + input.attr("pattern", options.pattern); + } + + if (options.maxlength) { + input.attr("maxlength", options.maxlength); + } + + // now place it in our form + form.append(input); + + form.on("submit", function(e) { + e.preventDefault(); + // Fix for SammyJS (or similar JS routing library) hijacking the form post. + e.stopPropagation(); + // @TODO can we actually click *the* button object instead? + // e.g. buttons.confirm.click() or similar + dialog.find(".btn-primary").click(); + }); + + dialog = exports.dialog(options); + + // clear the existing handler focusing the submit button... + dialog.off("shown.bs.modal"); + + // ...and replace it with one focusing our input, if possible + dialog.on("shown.bs.modal", function() { + // need the closure here since input isn't + // an object otherwise + input.focus(); + }); + + if (shouldShow === true) { + dialog.modal("show"); + } + + return dialog; + }; + + exports.dialog = function(options) { + options = sanitize(options); + + var dialog = $(templates.dialog); + var innerDialog = dialog.find(".modal-dialog"); + var body = dialog.find(".modal-body"); + var buttons = options.buttons; + var buttonStr = ""; + var callbacks = { + onEscape: options.onEscape + }; + + if ($.fn.modal === undefined) { + throw new Error( + "$.fn.modal is not defined; please double check you have included " + + "the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ " + + "for more details." + ); + } + + each(buttons, function(key, button) { + + // @TODO I don't like this string appending to itself; bit dirty. Needs reworking + // can we just build up button elements instead? slower but neater. Then button + // can just become a template too + buttonStr += ""; + callbacks[key] = button.callback; + }); + + body.find(".bootbox-body").html(options.message); + + if (options.animate === true) { + dialog.addClass("fade"); + } + + if (options.className) { + dialog.addClass(options.className); + } + + if (options.size === "large") { + innerDialog.addClass("modal-lg"); + } else if (options.size === "small") { + innerDialog.addClass("modal-sm"); + } + + if (options.title) { + body.before(templates.header); + } + + if (options.closeButton) { + var closeButton = $(templates.closeButton); + + if (options.title) { + dialog.find(".modal-header").prepend(closeButton); + } else { + closeButton.css("margin-top", "-10px").prependTo(body); + } + } + + if (options.title) { + dialog.find(".modal-title").html(options.title); + } + + if (buttonStr.length) { + body.after(templates.footer); + dialog.find(".modal-footer").html(buttonStr); + } + + + /** + * Bootstrap event listeners; used handle extra + * setup & teardown required after the underlying + * modal has performed certain actions + */ + + dialog.on("hidden.bs.modal", function(e) { + // ensure we don't accidentally intercept hidden events triggered + // by children of the current dialog. We shouldn't anymore now BS + // namespaces its events; but still worth doing + if (e.target === this) { + dialog.remove(); + } + }); + + /* + dialog.on("show.bs.modal", function() { + // sadly this doesn't work; show is called *just* before + // the backdrop is added so we'd need a setTimeout hack or + // otherwise... leaving in as would be nice + if (options.backdrop) { + dialog.next(".modal-backdrop").addClass("bootbox-backdrop"); + } + }); + */ + + dialog.on("shown.bs.modal", function() { + dialog.find(".btn-primary:first").focus(); + }); + + /** + * Bootbox event listeners; experimental and may not last + * just an attempt to decouple some behaviours from their + * respective triggers + */ + + if (options.backdrop !== "static") { + // A boolean true/false according to the Bootstrap docs + // should show a dialog the user can dismiss by clicking on + // the background. + // We always only ever pass static/false to the actual + // $.modal function because with `true` we can't trap + // this event (the .modal-backdrop swallows it) + // However, we still want to sort of respect true + // and invoke the escape mechanism instead + dialog.on("click.dismiss.bs.modal", function(e) { + // @NOTE: the target varies in >= 3.3.x releases since the modal backdrop + // moved *inside* the outer dialog rather than *alongside* it + if (dialog.children(".modal-backdrop").length) { + e.currentTarget = dialog.children(".modal-backdrop").get(0); + } + + if (e.target !== e.currentTarget) { + return; + } + + dialog.trigger("escape.close.bb"); + }); + } + + dialog.on("escape.close.bb", function(e) { + if (callbacks.onEscape) { + processCallback(e, dialog, callbacks.onEscape); + } + }); + + /** + * Standard jQuery event listeners; used to handle user + * interaction with our dialog + */ + + dialog.on("click", ".modal-footer button", function(e) { + var callbackKey = $(this).data("bb-handler"); + + processCallback(e, dialog, callbacks[callbackKey]); + }); + + dialog.on("click", ".bootbox-close-button", function(e) { + // onEscape might be falsy but that's fine; the fact is + // if the user has managed to click the close button we + // have to close the dialog, callback or not + processCallback(e, dialog, callbacks.onEscape); + }); + + dialog.on("keyup", function(e) { + if (e.which === 27) { + dialog.trigger("escape.close.bb"); + } + }); + + // the remainder of this method simply deals with adding our + // dialogent to the DOM, augmenting it with Bootstrap's modal + // functionality and then giving the resulting object back + // to our caller + + $(options.container).append(dialog); + + dialog.modal({ + backdrop: options.backdrop ? "static": false, + keyboard: false, + show: false + }); + + if (options.show) { + dialog.modal("show"); + } + + // @TODO should we return the raw element here or should + // we wrap it in an object on which we can expose some neater + // methods, e.g. var d = bootbox.alert(); d.hide(); instead + // of d.modal("hide"); + + /* + function BBDialog(elem) { + this.elem = elem; + } + + BBDialog.prototype = { + hide: function() { + return this.elem.modal("hide"); + }, + show: function() { + return this.elem.modal("show"); + } + }; + */ + + return dialog; + + }; + + exports.setDefaults = function() { + var values = {}; + + if (arguments.length === 2) { + // allow passing of single key/value... + values[arguments[0]] = arguments[1]; + } else { + // ... and as an object too + values = arguments[0]; + } + + $.extend(defaults, values); + }; + + exports.hideAll = function() { + $(".bootbox").modal("hide"); + + return exports; + }; + + + /** + * standard locales. Please add more according to ISO 639-1 standard. Multiple language variants are + * unlikely to be required. If this gets too large it can be split out into separate JS files. + */ + var locales = { + // ** removed all other languages + // ** use NodeBB translations instead + en : { + OK : "OK", + CANCEL : "Cancel", + CONFIRM : "OK" + }, + }; + + exports.addLocale = function(name, values) { + $.each(["OK", "CANCEL", "CONFIRM"], function(_, v) { + if (!values[v]) { + throw new Error("Please supply a translation for '" + v + "'"); + } + }); + + locales[name] = { + OK: values.OK, + CANCEL: values.CANCEL, + CONFIRM: values.CONFIRM + }; + + return exports; + }; + + exports.removeLocale = function(name) { + delete locales[name]; + + return exports; + }; + + exports.setLocale = function(name) { + return exports.setDefaults("locale", name); + }; + + exports.init = function(_$) { + return init(_$ || $); + }; + + return exports; +})); \ No newline at end of file diff --git a/public/vendor/bootbox/bootbox.min.js b/public/vendor/bootbox/bootbox.min.js deleted file mode 100644 index 1f85a1cf9e..0000000000 --- a/public/vendor/bootbox/bootbox.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * bootbox.js v4.4.0 - * - * http://bootboxjs.com/license.txt - * psychobunny - Removed require.js requirement - */ -!function(a,b){"use strict";a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d.call(c,a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"
",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:"static",animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback.call(this):!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,!1)},a.buttons.confirm.callback=function(){return a.callback.call(this,!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback.call(this,c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!b.isArray(k))throw new Error("Please pass an array of input options");if(!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),a.maxlength&&h.attr("maxlength",a.maxlength),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var d=b(n.dialog),f=d.find(".modal-dialog"),i=d.find(".modal-body"),j=a.buttons,k="",l={onEscape:a.onEscape};if(b.fn.modal===c)throw new Error("$.fn.modal is not defined; please double check you have included the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ for more details.");if(g(j,function(a,b){k+="",l[a]=b.callback}),i.find(".bootbox-body").html(a.message),a.animate===!0&&d.addClass("fade"),a.className&&d.addClass(a.className),"large"===a.size?f.addClass("modal-lg"):"small"===a.size&&f.addClass("modal-sm"),a.title&&i.before(n.header),a.closeButton){var m=b(n.closeButton);a.title?d.find(".modal-header").prepend(m):m.css("margin-top","-10px").prependTo(i)}return a.title&&d.find(".modal-title").html(a.title),k.length&&(i.after(n.footer),d.find(".modal-footer").html(k)),d.on("hidden.bs.modal",function(a){a.target===this&&d.remove()}),d.on("shown.bs.modal",function(){d.find(".btn-primary:first").focus()}),"static"!==a.backdrop&&d.on("click.dismiss.bs.modal",function(a){d.children(".modal-backdrop").length&&(a.currentTarget=d.children(".modal-backdrop").get(0)),a.target===a.currentTarget&&d.trigger("escape.close.bb")}),d.on("escape.close.bb",function(a){l.onEscape&&e(a,d,l.onEscape)}),d.on("click",".modal-footer button",function(a){var c=b(this).data("bb-handler");e(a,d,l[c])}),d.on("click",".bootbox-close-button",function(a){e(a,d,l.onEscape)}),d.on("keyup",function(a){27===a.which&&d.trigger("escape.close.bb")}),b(a.container).append(d),d.modal({backdrop:a.backdrop?"static":!1,keyboard:!1,show:!1}),a.show&&d.modal("show"),d},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={bg_BG:{OK:"Ок",CANCEL:"Отказ",CONFIRM:"Потвърждавам"},br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fa:{OK:"قبول",CANCEL:"لغو",CONFIRM:"تایید"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},hu:{OK:"OK",CANCEL:"Mégsem",CONFIRM:"Megerősít"},hr:{OK:"OK",CANCEL:"Odustani",CONFIRM:"Potvrdi"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"確認"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sq:{OK:"OK",CANCEL:"Anulo",CONFIRM:"Prano"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},th:{OK:"ตกลง",CANCEL:"ยกเลิก",CONFIRM:"ยืนยัน"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.addLocale=function(a,c){return b.each(["OK","CANCEL","CONFIRM"],function(a,b){if(!c[b])throw new Error("Please supply a translation for '"+b+"'")}),q[a]={OK:c.OK,CANCEL:c.CANCEL,CONFIRM:c.CONFIRM},p},p.removeLocale=function(a){return delete q[a],p},p.setLocale=function(a){return p.setDefaults("locale",a)},p.init=function(c){return a(c||b)},p}); \ No newline at end of file diff --git a/public/vendor/bootbox/wrapper.js b/public/vendor/bootbox/wrapper.js new file mode 100644 index 0000000000..6988c4398d --- /dev/null +++ b/public/vendor/bootbox/wrapper.js @@ -0,0 +1,44 @@ +/* global bootbox */ + +require(['translator'], function (shim) { + "use strict"; + + var translator = shim.Translator.create(); + var dialog = bootbox.dialog; + bootbox.dialog = function (options) { + var translate = [ + translator.translate(options.message), + options.title && translator.translate(options.title), + ].concat(Object.keys(options.buttons).map(function (key) { + if (/cancel|confirm|ok/.test(key)) { + return null; + } + return translator.translate(options.buttons[key].label).then(function (label) { + options.buttons[key].label = label; + }); + })); + + Promise.all(translate).then(function (translations) { + options.message = translations[0]; + options.title = translations[1]; + + dialog.call(bootbox, options); + }); + }; + + Promise.all([ + translator.translateKey('modules:bootbox.ok', []), + translator.translateKey('modules:bootbox.cancel', []), + translator.translateKey('modules:bootbox.confirm', []), + ]).then(function (translations) { + var lang = shim.getLanguage(); + bootbox.addLocale(lang, { + OK: translations[0], + CANCEL: translations[1], + CONFIRM: translations[2], + }); + + bootbox.setLocale(lang); + }); +}); + diff --git a/src/meta/js.js b/src/meta/js.js index 626fa0ecd8..05f045f283 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -25,7 +25,8 @@ module.exports = function (Meta) { 'public/vendor/jquery/textcomplete/jquery.textcomplete.js', 'public/vendor/requirejs/require.js', 'public/src/require-config.js', - 'public/vendor/bootbox/bootbox.min.js', + 'public/vendor/bootbox/bootbox.js', + 'public/vendor/bootbox/wrapper.js', 'public/vendor/tinycon/tinycon.js', 'public/vendor/xregexp/xregexp.js', 'public/vendor/xregexp/unicode/unicode-base.js', From 606de990e941fff4ab3b6a59cf838b12a51838dd Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 8 Dec 2016 02:06:25 -0700 Subject: [PATCH 12/28] `admin/advanced` JS translations --- public/language/en-GB/admin/advanced/errors.json | 4 +++- public/language/en-GB/admin/advanced/logs.json | 3 ++- public/src/admin/advanced/errors.js | 6 +++--- public/src/admin/advanced/logs.js | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/public/language/en-GB/admin/advanced/errors.json b/public/language/en-GB/admin/advanced/errors.json index dc9425b4c4..963e68b116 100644 --- a/public/language/en-GB/admin/advanced/errors.json +++ b/public/language/en-GB/admin/advanced/errors.json @@ -8,5 +8,7 @@ "clear-error-log": "Clear Error Log", "route": "Route", "count": "Count", - "no-routes-not-found": "Hooray! There are no routes that were not found." + "no-routes-not-found": "Hooray! There are no routes that were not found.", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" } \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/logs.json b/public/language/en-GB/admin/advanced/logs.json index 7426b14a36..b9de400e1c 100644 --- a/public/language/en-GB/admin/advanced/logs.json +++ b/public/language/en-GB/admin/advanced/logs.json @@ -2,5 +2,6 @@ "logs": "Logs", "control-panel": "Logs Control Panel", "reload": "Reload Logs", - "clear": "Clear Logs" + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" } \ No newline at end of file diff --git a/public/src/admin/advanced/errors.js b/public/src/admin/advanced/errors.js index 29ee7e362c..d4cfccb7bd 100644 --- a/public/src/admin/advanced/errors.js +++ b/public/src/admin/advanced/errors.js @@ -1,7 +1,7 @@ "use strict"; /*global config, define, app, socket, ajaxify, bootbox, templates, Chart, utils */ -define('admin/advanced/errors', ['Chart'], function (Chart) { +define('admin/advanced/errors', ['Chart', 'translator'], function (Chart, translator) { var Errors = {}; Errors.init = function () { @@ -11,7 +11,7 @@ define('admin/advanced/errors', ['Chart'], function (Chart) { }; Errors.clear404 = function () { - bootbox.confirm('Are you sure you wish to clear the 404 error logs?', function (ok) { + bootbox.confirm('[[admin/advanced/errors:clear404-confirm]]', function (ok) { if (ok) { socket.emit('admin.errors.clear', {}, function (err) { if (err) { @@ -19,7 +19,7 @@ define('admin/advanced/errors', ['Chart'], function (Chart) { } ajaxify.refresh(); - app.alertSuccess('"404 Not Found" errors cleared'); + app.alertSuccess('[[admin/advanced/errors:clear404-success]]'); }); } }); diff --git a/public/src/admin/advanced/logs.js b/public/src/admin/advanced/logs.js index 2ea10f1b36..667dcd7280 100644 --- a/public/src/admin/advanced/logs.js +++ b/public/src/admin/advanced/logs.js @@ -29,7 +29,7 @@ define('admin/advanced/logs', function () { case 'clear': socket.emit('admin.logs.clear', function (err) { if (!err) { - app.alertSuccess('Logs Cleared!'); + app.alertSuccess('[[admin/advanced/logs:clear-success]]'); btnEl.prev().click(); } }); From bae1daf5dca2b059d02dc68460729e8382b762ee Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 8 Dec 2016 16:39:02 -0700 Subject: [PATCH 13/28] Bootbox wrapper improvements --- public/src/modules/translator.js | 4 +-- public/vendor/bootbox/wrapper.js | 57 +++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index c19c705eaf..0c960f2234 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -22,8 +22,8 @@ if (global.env === 'development') { var winston = require('winston'); - warn = function (a, b, c, d) { - winston.warn(a, b, c, d); + warn = function (a) { + winston.warn(a); }; } diff --git a/public/vendor/bootbox/wrapper.js b/public/vendor/bootbox/wrapper.js index 6988c4398d..bfc9457241 100644 --- a/public/vendor/bootbox/wrapper.js +++ b/public/vendor/bootbox/wrapper.js @@ -3,27 +3,52 @@ require(['translator'], function (shim) { "use strict"; + function descendantTextNodes(node) { + var textNodes = []; + + function helper(node) { + if (node.nodeType === 3) { + textNodes.push(node); + } else { + for (var i = 0, c = node.childNodes, l = c.length; i < l; i += 1) { + helper(c[i]); + } + } + } + + helper(node); + return textNodes; + } + var translator = shim.Translator.create(); var dialog = bootbox.dialog; bootbox.dialog = function (options) { - var translate = [ - translator.translate(options.message), - options.title && translator.translate(options.title), - ].concat(Object.keys(options.buttons).map(function (key) { - if (/cancel|confirm|ok/.test(key)) { - return null; - } - return translator.translate(options.buttons[key].label).then(function (label) { - options.buttons[key].label = label; - }); - })); + var show, $elem, nodes, text; - Promise.all(translate).then(function (translations) { - options.message = translations[0]; - options.title = translations[1]; + show = options.show !== false; + options.show = false; - dialog.call(bootbox, options); - }); + $elem = dialog.call(bootbox, options); + + if (/\[\[[a-zA-Z0-9\-_.\/:]+\]\]/.test($elem[0].outerHTML)) { + nodes = descendantTextNodes($elem[0]); + text = nodes.map(function (node) { + return node.nodeValue; + }).join(' || '); + + translator.translate(text).then(function (translated) { + translated.split(' || ').forEach(function (str, i) { + nodes[i].nodeValue = str; + }); + if (show) { + $elem.modal('show'); + } + }); + } else if (show) { + $elem.modal('show'); + } + + return $elem; }; Promise.all([ From 38eba81933792b44de07a53ef36072430c03e9b8 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 8 Dec 2016 17:10:37 -0700 Subject: [PATCH 14/28] ACP menu and title translations --- public/language/en-GB/admin/admin.json | 6 + public/language/en-GB/admin/menu.json | 74 +++++++++ public/src/admin/admin.js | 91 +++++----- src/controllers/admin/blacklist.js | 2 +- src/controllers/admin/flags.js | 1 - src/views/admin/partials/menu.tpl | 222 ++++++++++++------------- 6 files changed, 243 insertions(+), 153 deletions(-) create mode 100644 public/language/en-GB/admin/admin.json create mode 100644 public/language/en-GB/admin/menu.json diff --git a/public/language/en-GB/admin/admin.json b/public/language/en-GB/admin/admin.json new file mode 100644 index 0000000000..a64cc856a6 --- /dev/null +++ b/public/language/en-GB/admin/admin.json @@ -0,0 +1,6 @@ +{ + "alert.confirm-reload": "Are you sure you wish to reload NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json new file mode 100644 index 0000000000..1db5c15abf --- /dev/null +++ b/public/language/en-GB/admin/menu.json @@ -0,0 +1,74 @@ +{ + "section-general": "General", + "general/dashboard": "Dashboard", + "general/homepage": "Home Page", + "general/navigation": "Navigation", + "general/languages": "Languages", + "general/sounds": "Sounds", + "general/social": "Social", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/registration": "Registration Queue", + "manage/groups": "Groups", + "manage/flags": "Flags", + "manage/ip-blacklist": "IP Blacklist", + + "section-settings": "Settings", + "settings/general": "General", + "settings/reputation": "Reputation", + "settings/email": "Email", + "settings/user": "User", + "settings/group": "Group", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/post": "Post", + "settings/chat": "Chat", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom HTML & CSS", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + + "reload-forum": "Reload Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search...", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect..." +} \ No newline at end of file diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index c7a874e96a..6dabcb9f38 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -9,16 +9,12 @@ } logoutTimer = setTimeout(function () { - require(['translator'], function (translator) { - translator.translate('[[login:logged-out-due-to-inactivity]]', function (translated) { - bootbox.alert({ - closeButton: false, - message: translated, - callback: function () { - window.location.reload(); - } - }); - }); + bootbox.alert({ + closeButton: false, + message: '[[login:logged-out-due-to-inactivity]]', + callback: function () { + window.location.reload(); + } }); }, 3600000); } @@ -69,11 +65,9 @@ } function setupKeybindings() { - require(['mousetrap'], function (mousetrap) { + require(['mousetrap', 'admin/modules/instance'], function (mousetrap, instance) { mousetrap.bind('ctrl+shift+a r', function () { - require(['admin/modules/instance'], function (instance) { - instance.reload(); - }); + instance.reload(); }); mousetrap.bind('ctrl+shift+a R', function () { @@ -89,43 +83,60 @@ } function selectMenuItem(url) { - url = url - .replace(/\/\d+$/, '') - .split('/').slice(0, 3).join('/') - .split('?')[0]; - - // If index is requested, load the dashboard - if (url === 'admin') { - url = 'admin/general/dashboard'; - } + require(['translator'], function (translator) { + url = url + .replace(/\/\d+$/, '') + .split('/').slice(0, 3).join('/') + .split('?')[0]; + + // If index is requested, load the dashboard + if (url === 'admin') { + url = 'admin/general/dashboard'; + } - $('#main-menu li').removeClass('active'); - $('#main-menu a').removeClass('active').each(function () { - var menu = $(this), - href = menu.attr('href'), - isLink = menu.parent().attr('data-link') === '1'; + url = [config.relative_path, url].join('/'); - if (!isLink && href && href === [config.relative_path, url].join('/')) { + $('#main-menu li').removeClass('active'); + $('#main-menu a').removeClass('active').filter('[href="' + url + '"]').each(function () { + var menu = $(this); menu .parent().addClass('active') .parents('.menu-item').addClass('active'); + + var match = menu.attr('href').match(/admin\/((.+?)\/.+?)$/); + if (!match) { + return; + } + var str = '[[admin/menu:' + match[1] + ']]'; + if (match[2] === 'settings') { + str = translator.compile('admin/menu:settings.page-title', str); + } + translator.translate(str, function (text) { + $('#main-page-title').text(text); + }); + }); - $('#main-page-title').text(menu.text() + (menu.parents('.menu-item').children('a').text() === 'Settings' ? ' Settings' : '')); + var title; + if (/admin\/general\/dashboard$/.test(url)) { + title = '[[admin/menu:general/dashboard]]'; + } else { + title = url.match(/admin\/(.+?)\/(.+?)$/); + title = '[[admin/menu:section-' + title[1] + ']]' + + (title[2] ? (' > [[admin/menu:' + + title[1] + '/' + title[2] + ']]') : ''); } - }); - var acpPath = url.replace('admin/', '').split('/'); - acpPath.forEach(function (path, i) { - acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1); - }); - acpPath = acpPath.join(' > '); + title = '[[admin/admin:acp-title, ' + title + ']]'; - document.title = (url === 'admin/general/dashboard' ? 'Dashboard' : acpPath) + ' | NodeBB Admin Control Panel'; + translator.translate(title, function (title) { + document.title = title.replace(/>/g, '>'); + }); + }); } function setupRestartLinks() { $('.reload').off('click').on('click', function () { - bootbox.confirm('Are you sure you wish to reload NodeBB?', function (confirm) { + bootbox.confirm('[[admin/admin:alert.confirm-reload]]', function (confirm) { if (confirm) { require(['admin/modules/instance'], function (instance) { instance.reload(); @@ -135,7 +146,7 @@ }); $('.restart').off('click').on('click', function () { - bootbox.confirm('Are you sure you wish to restart NodeBB?', function (confirm) { + bootbox.confirm('[[admin/admin:alert.confirm-restart]]', function (confirm) { if (confirm) { require(['admin/modules/instance'], function (instance) { instance.restart(); @@ -143,7 +154,7 @@ } }); }); - }; + } function launchSnackbar(params) { var message = (params.title ? "" + params.title + "" : '') + (params.message ? params.message : ''); diff --git a/src/controllers/admin/blacklist.js b/src/controllers/admin/blacklist.js index d70b1d1d79..c9ba71169e 100644 --- a/src/controllers/admin/blacklist.js +++ b/src/controllers/admin/blacklist.js @@ -9,7 +9,7 @@ blacklistController.get = function (req, res, next) { if (err) { return next(err); } - res.render('admin/manage/ip-blacklist', {rules: rules, title: 'IP Blacklist'}); + res.render('admin/manage/ip-blacklist', { rules: rules }); }); }; diff --git a/src/controllers/admin/flags.js b/src/controllers/admin/flags.js index 1b31a95ff4..03c9329fca 100644 --- a/src/controllers/admin/flags.js +++ b/src/controllers/admin/flags.js @@ -65,7 +65,6 @@ flagsController.get = function (req, res, next) { sortByCount: sortBy === 'count', sortByTime: sortBy === 'time', pagination: pagination.create(page, pageCount, req.query), - title: '[[pages:flagged-posts]]' }; res.render('admin/manage/flags', data); }); diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 9bc0c48d7e..ea481dcfb8 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -1,72 +1,72 @@
diff --git a/src/views/admin/partials/installed_plugin_item.tpl b/src/views/admin/partials/installed_plugin_item.tpl index cd34098616..b76ae9f964 100644 --- a/src/views/admin/partials/installed_plugin_item.tpl +++ b/src/views/admin/partials/installed_plugin_item.tpl @@ -2,7 +2,7 @@
  • - [[admin/extend/plugins:themes]] + [[admin/extend/plugins:plugin-item.themes]] From 30087947973a69a3a4547a8bd0edd97c99b2f044 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 8 Dec 2016 17:17:24 -0700 Subject: [PATCH 16/28] `admin/general` JS translations and misc --- .../en-GB/admin/general/dashboard.json | 15 +- .../language/en-GB/admin/general/social.json | 3 +- .../language/en-GB/admin/general/sounds.json | 3 +- public/src/admin/general/dashboard.js | 266 ++++++++++-------- public/src/admin/general/navigation.js | 4 +- public/src/admin/general/social.js | 2 +- public/src/admin/general/sounds.js | 2 +- src/views/admin/general/dashboard.tpl | 2 +- src/views/admin/general/navigation.tpl | 2 +- 9 files changed, 167 insertions(+), 132 deletions(-) diff --git a/public/language/en-GB/admin/general/dashboard.json b/public/language/en-GB/admin/general/dashboard.json index dab42d9d00..b82802db1b 100644 --- a/public/language/en-GB/admin/general/dashboard.json +++ b/public/language/en-GB/admin/general/dashboard.json @@ -14,6 +14,10 @@ "updates": "Updates", "running-version": "You are running NodeBB v%1.", "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

    You are up-to-date

    ", + "upgrade-available": "

    A new version (v%1) has been released. Consider upgrading your NodeBB.

    ", + "prerelease-upgrade-available": "

    This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

    ", + "prerelease-warning": "

    This is a pre-release version of NodeBB. Unintended bugs may occur.

    ", "notices": "Notices", @@ -26,6 +30,10 @@ "realtime-chart-updates": "Realtime Chart Updates", "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", "anonymous-registered-users": "Anonymous vs Registered Users", "anonymous": "Anonymous", @@ -38,5 +46,10 @@ "recent": "Recent", "unread": "Unread", - "high-presence-topics": "High Presence Topics" + "high-presence-topics": "High Presence Topics", + + "graphs.page-views": "Page Views", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.anonymous-users": "Anonymous Users" } \ No newline at end of file diff --git a/public/language/en-GB/admin/general/social.json b/public/language/en-GB/admin/general/social.json index 257e20b54b..23aedfcfaa 100644 --- a/public/language/en-GB/admin/general/social.json +++ b/public/language/en-GB/admin/general/social.json @@ -1,4 +1,5 @@ { "post-sharing": "Post Sharing", - "info-plugins-additional": "Plugins can add additional networks for sharing posts." + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" } \ No newline at end of file diff --git a/public/language/en-GB/admin/general/sounds.json b/public/language/en-GB/admin/general/sounds.json index 42860f4b1c..95ccbde0f1 100644 --- a/public/language/en-GB/admin/general/sounds.json +++ b/public/language/en-GB/admin/general/sounds.json @@ -4,5 +4,6 @@ "play-sound": "Play", "incoming-message": "Incoming Message", "outgoing-message": "Outgoing Message", - "upload-new-sound": "Upload New Sound" + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" } \ No newline at end of file diff --git a/public/src/admin/general/dashboard.js b/public/src/admin/general/dashboard.js index 62ef80313a..f17be6fde2 100644 --- a/public/src/admin/general/dashboard.js +++ b/public/src/admin/general/dashboard.js @@ -1,22 +1,22 @@ "use strict"; /*global define, ajaxify, app, socket, utils, bootbox, RELATIVE_PATH*/ -define('admin/general/dashboard', ['semver', 'Chart'], function (semver, Chart) { +define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (semver, Chart, translator) { var Admin = {}; var intervals = { - rooms: false, - graphs: false - }; + rooms: false, + graphs: false + }; var isMobile = false; var isPrerelease = /^v?\d+\.\d+\.\d+-.+$/; var graphData = { - rooms: {}, - traffic: {} - }; + rooms: {}, + traffic: {} + }; var currentGraph = { - units: 'hours', - until: undefined - }; + units: 'hours', + until: undefined + }; var DEFAULTS = { roomInterval: 10000, @@ -53,23 +53,28 @@ define('admin/general/dashboard', ['semver', 'Chart'], function (semver, Chart) var version = $('#version').html(), latestVersion = releases[0].name.slice(1), - checkEl = $('.version-check'); + checkEl = $('.version-check'), + text; // Alter box colour accordingly if (semver.eq(latestVersion, version)) { checkEl.removeClass('alert-info').addClass('alert-success'); - checkEl.append('

    You are up-to-date

    '); + text = '[[admin/general/dashboard:up-to-date]]'; } else if (semver.gt(latestVersion, version)) { checkEl.removeClass('alert-info').addClass('alert-warning'); if (!isPrerelease.test(version)) { - checkEl.append('

    A new version (v' + latestVersion + ') has been released. Consider upgrading your NodeBB.

    '); + text = '[[admin/general/dashboard:upgrade-available, ' + latestVersion + ']]'; } else { - checkEl.append('

    This is an outdated pre-release version of NodeBB. A new version (v' + latestVersion + ') has been released. Consider upgrading your NodeBB.

    '); + text = '[[admin/general/dashboard:prerelease-upgrade-available, ' + latestVersion + ']]'; } } else if (isPrerelease.test(version)) { checkEl.removeClass('alert-info').addClass('alert-info'); - checkEl.append('

    This is a pre-release version of NodeBB. Unintended bugs may occur. .

    '); + text = '[[admin/general/dashboard:prerelease-warning]]'; } + + translator.translate(text, function (text) { + checkEl.append(text); + }); }); $('[data-toggle="tooltip"]').tooltip(); @@ -92,26 +97,28 @@ define('admin/general/dashboard', ['semver', 'Chart'], function (semver, Chart) var html = '
    ' + '
    ' + data.onlineRegisteredCount + '
    ' + - '
    Users
    ' + + '
    [[admin/general/dashboard:active-users.users]]
    ' + '
    ' + '
    ' + '
    ' + data.onlineGuestCount + '
    ' + - '
    Guests
    ' + + '
    [[admin/general/dashboard:active-users.guests]]
    ' + '
    ' + '
    ' + '
    ' + (data.onlineRegisteredCount + data.onlineGuestCount) + '
    ' + - '
    Total
    ' + + '
    [[admin/general/dashboard:active-users.total]]
    ' + '
    ' + '
    ' + '
    ' + data.socketCount + '
    ' + - '
    Connections
    ' + + '
    [[admin/general/dashboard:active-users.connections]]
    ' + '
    '; updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount); updatePresenceGraph(data.users); updateTopicsGraph(data.topics); - $('#active-users').html(html); + translator.translate(html, function (html) { + $('#active-users').html(html); + }); }; var graphs = { @@ -168,119 +175,132 @@ define('admin/general/dashboard', ['semver', 'Chart'], function (semver, Chart) Chart.defaults.global.tooltips.enabled = false; } - var data = { - labels: trafficLabels, - datasets: [ - { - label: "Page Views", - backgroundColor: "rgba(220,220,220,0.2)", - borderColor: "rgba(220,220,220,1)", - pointBackgroundColor: "rgba(220,220,220,1)", - pointHoverBackgroundColor: "#fff", - pointBorderColor: "#fff", - pointHoverBorderColor: "rgba(220,220,220,1)", - data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - }, - { - label: "Unique Visitors", - backgroundColor: "rgba(151,187,205,0.2)", - borderColor: "rgba(151,187,205,1)", - pointBackgroundColor: "rgba(151,187,205,1)", - pointHoverBackgroundColor: "#fff", - pointBorderColor: "#fff", - pointHoverBorderColor: "rgba(151,187,205,1)", - data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + var t = translator.Translator.create(); + Promise.all([ + t.translateKey('admin/general/dashboard:graphs.page-views', []), + t.translateKey('admin/general/dashboard:graphs.unique-visitors', []), + t.translateKey('admin/general/dashboard:graphs.registered-users', []), + t.translateKey('admin/general/dashboard:graphs.anonymous-users', []), + t.translateKey('admin/general/dashboard:on-categories', []), + t.translateKey('admin/general/dashboard:reading-posts', []), + t.translateKey('admin/general/dashboard:browsing-topics', []), + t.translateKey('admin/general/dashboard:recent', []), + t.translateKey('admin/general/dashboard:unread', []), + ]).then(function (translations) { + var data = { + labels: trafficLabels, + datasets: [ + { + label: translations[0], + backgroundColor: "rgba(220,220,220,0.2)", + borderColor: "rgba(220,220,220,1)", + pointBackgroundColor: "rgba(220,220,220,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(220,220,220,1)", + data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + }, + { + label: translations[1], + backgroundColor: "rgba(151,187,205,0.2)", + borderColor: "rgba(151,187,205,1)", + pointBackgroundColor: "rgba(151,187,205,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(151,187,205,1)", + data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + } + ] + }; + + trafficCanvas.width = $(trafficCanvas).parent().width(); + graphs.traffic = new Chart(trafficCtx, { + type: 'line', + data: data, + options: { + responsive: true, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } } - ] - }; - - trafficCanvas.width = $(trafficCanvas).parent().width(); - graphs.traffic = new Chart(trafficCtx, { - type: 'line', - data: data, - options: { - responsive: true, - legend: { - display: false - }, - scales: { - yAxes: [{ - ticks: { - beginAtZero: true - } + }); + + graphs.registered = new Chart(registeredCtx, { + type: 'doughnut', + data: { + labels: translations.slice(2, 4), + datasets: [{ + data: [1, 1], + backgroundColor: ["#F7464A", "#46BFBD"], + hoverBackgroundColor: ["#FF5A5E", "#5AD3D1"] }] + }, + options: { + responsive: true, + legend: { + display: false + } } - } - }); - - graphs.registered = new Chart(registeredCtx, { - type: 'doughnut', - data: { - labels: ["Registered Users", "Anonymous Users"], - datasets: [{ - data: [1, 1], - backgroundColor: ["#F7464A", "#46BFBD"], - hoverBackgroundColor: ["#FF5A5E", "#5AD3D1"] - }] - }, - options: { - responsive: true, - legend: { - display: false - } - } - }); + }); - graphs.presence = new Chart(presenceCtx, { - type: 'doughnut', - data: { - labels: ["On categories list", "Reading posts", "Browsing topics", "Recent", "Unread"], - datasets: [{ - data: [1, 1, 1, 1, 1], - backgroundColor: ["#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#9FB194"], - hoverBackgroundColor: ["#FF5A5E", "#5AD3D1", "#FFC870", "#A8B3C5", "#A8B3C5"] - }] - }, - options: { - responsive: true, - legend: { - display: false + graphs.presence = new Chart(presenceCtx, { + type: 'doughnut', + data: { + labels: translations.slice(4, 9), + datasets: [{ + data: [1, 1, 1, 1, 1], + backgroundColor: ["#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#9FB194"], + hoverBackgroundColor: ["#FF5A5E", "#5AD3D1", "#FFC870", "#A8B3C5", "#A8B3C5"] + }] + }, + options: { + responsive: true, + legend: { + display: false + } } - } - }); + }); - graphs.topics = new Chart(topicsCtx, { - type: 'doughnut', - data: { - labels: [], - datasets: [{ - data: [], - backgroundColor: [], - hoverBackgroundColor: [] - }] - }, - options: { - responsive: true, - legend: { - display: false + graphs.topics = new Chart(topicsCtx, { + type: 'doughnut', + data: { + labels: [], + datasets: [{ + data: [], + backgroundColor: [], + hoverBackgroundColor: [] + }] + }, + options: { + responsive: true, + legend: { + display: false + } } - } - }); + }); - updateTrafficGraph(); + updateTrafficGraph(); - $(window).on('resize', adjustPieCharts); - adjustPieCharts(); + $(window).on('resize', adjustPieCharts); + adjustPieCharts(); - $('[data-action="updateGraph"]').on('click', function () { - var until; - switch($(this).attr('data-until')) { - case 'last-month': - var lastMonth = new Date(); - lastMonth.setDate(lastMonth.getDate() - 30); - until = lastMonth.getTime(); - } - updateTrafficGraph($(this).attr('data-units'), until); + $('[data-action="updateGraph"]').on('click', function () { + var until; + switch($(this).attr('data-until')) { + case 'last-month': + var lastMonth = new Date(); + lastMonth.setDate(lastMonth.getDate() - 30); + until = lastMonth.getTime(); + } + updateTrafficGraph($(this).attr('data-units'), until); + }); }); } diff --git a/public/src/admin/general/navigation.js b/public/src/admin/general/navigation.js index 4c4256ddd1..3476780942 100644 --- a/public/src/admin/general/navigation.js +++ b/public/src/admin/general/navigation.js @@ -12,8 +12,8 @@ define('admin/general/navigation', ['translator', 'iconSelect', 'jqueryui'], fun $(this).val(translator.unescape($(this).val())); }); - translator.translate(translator.unescape($('#available').html()), function (html) { - $('#available').html(html) + translator.translate($('#available').html(), function (html) { + $('#available').html(translator.unescape(html)) .find('li .drag-item').draggable({ connectToSortable: '#active-navigation', helper: 'clone', diff --git a/public/src/admin/general/social.js b/public/src/admin/general/social.js index 9accf87417..cfcc3d3c52 100644 --- a/public/src/admin/general/social.js +++ b/public/src/admin/general/social.js @@ -18,7 +18,7 @@ define('admin/general/social', [], function () { return app.alertError(err); } - app.alertSuccess('Successfully saved Post Sharing Networks!'); + app.alertSuccess('[[admin/general/social:save-success]]'); }); }); }; diff --git a/public/src/admin/general/sounds.js b/public/src/admin/general/sounds.js index 0ea87f0917..64926e60db 100644 --- a/public/src/admin/general/sounds.js +++ b/public/src/admin/general/sounds.js @@ -23,7 +23,7 @@ define('admin/general/sounds', ['sounds', 'settings'], function (Sounds, Setting socket.emit('admin.fireEvent', { name: 'event:sounds.reloadMapping' }); - app.alertSuccess('Settings Saved'); + app.alertSuccess('[[admin/general/sounds:saved]]'); }); }); }; diff --git a/src/views/admin/general/dashboard.tpl b/src/views/admin/general/dashboard.tpl index 2e42dd8430..cc1d8d97ce 100644 --- a/src/views/admin/general/dashboard.tpl +++ b/src/views/admin/general/dashboard.tpl @@ -104,7 +104,7 @@ [[admin/general/dashboard:restart-warning]]

    - [[maintenance-mode]] + [[admin/general/dashboard:maintenance-mode]]


    diff --git a/src/views/admin/general/navigation.tpl b/src/views/admin/general/navigation.tpl index 5143bea7b2..3d3d17f096 100644 --- a/src/views/admin/general/navigation.tpl +++ b/src/views/admin/general/navigation.tpl @@ -127,7 +127,7 @@

    {available.text} {available.route}
    - [[admin/general/navigation:core]] [[plugin]] + [[admin/general/navigation:core]] [[admin/general/navigation:plugin]]

  • From 862908d0eb7ba647922f2b5c3bb34661d73cd951 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 8 Dec 2016 19:40:57 -0700 Subject: [PATCH 17/28] ACP search and title improvements - Search uses translated titles if available - Use `advanced` for `development` route titles - Remove route title from showing up in results - Highlight matching part of result title - Don't show empty result contents when only title is matched --- public/language/en-GB/admin/menu.json | 1 + public/src/admin/admin.js | 11 +++++---- public/src/admin/modules/search.js | 26 +++++++++++---------- src/admin/search.js | 33 ++++++++++++++++++++------- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 1db5c15abf..7a5327f643 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -58,6 +58,7 @@ "advanced/errors": "Errors", "advanced/cache": "Cache", "development/logger": "Logger", + "development/info": "Info", "reload-forum": "Reload Forum", "restart-forum": "Restart Forum", diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index 6dabcb9f38..217438e146 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -116,13 +116,14 @@ }); }); - var title; - if (/admin\/general\/dashboard$/.test(url)) { + var title = url; + if (/admin\/general\/dashboard$/.test(title)) { title = '[[admin/menu:general/dashboard]]'; } else { - title = url.match(/admin\/(.+?)\/(.+?)$/); - title = '[[admin/menu:section-' + title[1] + ']]' + - (title[2] ? (' > [[admin/menu:' + + title = title.match(/admin\/(.+?)\/(.+?)$/); + title = '[[admin/menu:section-' + + (title[1] === 'development' ? 'advanced' : title[1]) + + ']]' + (title[2] ? (' > [[admin/menu:' + title[1] + '/' + title[2] + ']]') : ''); } diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js index 4f46898b8e..29379f0dac 100644 --- a/public/src/admin/modules/search.js +++ b/public/src/admin/modules/search.js @@ -4,39 +4,41 @@ define('admin/modules/search', ['mousetrap'], function (mousetrap) { var search = {}; - function nsToTitle(namespace) { - return namespace.replace('admin/', '').split('/').map(function (str) { - return str[0].toUpperCase() + str.slice(1); - }).join(' > '); - } - function find(dict, term) { var html = dict.filter(function (elem) { return elem.translations.toLowerCase().includes(term); }).map(function (params) { var namespace = params.namespace; var translations = params.translations; - var title = params.title == null ? nsToTitle(namespace) : params.title; + var title = params.title; var results = translations // remove all lines without a match .replace(new RegExp('^(?:(?!' + term + ').)*$', 'gmi'), '') - // get up to 25 characaters of context on both sides of the match + // remove lines that only match the title + .replace(new RegExp('(^|\\n).*?' + title + '.*?(\\n|$)', 'g'), '') + // get up to 25 characters of context on both sides of the match // and wrap the match in a `.search-match` element .replace( new RegExp('^[\\s\\S]*?(.{0,25})(' + term + ')(.{0,25})[\\s\\S]*?$', 'gmi'), '...$1$2$3...
    ' ) // collapse whitespace - .replace(/(?:\n ?)+/g, '\n'); + .replace(/(?:\n ?)+/g, '\n') + .trim(); + + title = title.replace( + new RegExp('(^.*?)(' + term + ')(.*?$)', 'gi'), + '$1$2$3' + ); return ''; }).join(''); diff --git a/src/admin/search.js b/src/admin/search.js index 8f567071bf..0bd140e3ba 100644 --- a/src/admin/search.js +++ b/src/admin/search.js @@ -54,7 +54,7 @@ function simplify(translations) { function nsToTitle(namespace) { return namespace.replace('admin/', '').split('/').map(function (str) { return str[0].toUpperCase() + str.slice(1); - }).join(' > '); + }).join(' > ').replace(/[^a-zA-Z> ]/g, ' '); } var fallbackCacheInProgress = {}; @@ -67,15 +67,17 @@ function initFallback(namespace, callback) { } var template = file.toString(); + var title = nsToTitle(namespace); var translations = sanitize(template); translations = Translator.removePatterns(translations); translations = simplify(translations); - translations += '\n' + nsToTitle(namespace); + translations += '\n' + title; callback(null, { namespace: namespace, translations: translations, + title: title, }); }); } @@ -124,17 +126,32 @@ function initDict(language, callback) { var str = Object.keys(translations).map(function (key) { return translations[key]; }).join('\n'); + str = sanitize(str); + + var title = namespace; + if (/admin\/general\/dashboard$/.test(title)) { + title = '[[admin/menu:general/dashboard]]'; + } else { + title = title.match(/admin\/(.+?)\/(.+?)$/); + title = '[[admin/menu:section-' + + (title[1] === 'development' ? 'advanced' : title[1]) + + ']]' + (title[2] ? (' > [[admin/menu:' + + title[1] + '/' + title[2] + ']]') : ''); + } - next(null, { - namespace: namespace, - translations: str, - }); - } + Translator.create(language).translate(title).then(function (title) { + next(null, { + namespace: namespace, + translations: str + '\n' + title, + title: title, + }); + }).catch(err); + }, ], function (err, params) { if (err) { return fallback(namespace, function (err, params) { if (err) { - return cb({ + return cb(null, { namespace: namespace, translations: '', }); From 94eb74646cce17f15d495b08c3c06e111278513f Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 9 Dec 2016 20:47:28 -0700 Subject: [PATCH 18/28] `admin/manage/categories` translations - Fix privilege table headers so bottom borders align - Fix `/admin` route to show Dashboard title correctly - Translate ACP category management and privileges templates - Translate ACP category management JS - Remove unnecessary translates in JS - Fix bootbox wrapper to work with translations containing html --- .../en-GB/admin/manage/categories.json | 68 +++++++++++ public/less/admin/manage/categories.less | 1 + public/src/admin/admin.js | 2 +- public/src/admin/manage/categories.js | 56 ++++----- public/src/admin/manage/category.js | 108 ++++++++++++------ public/vendor/bootbox/wrapper.js | 6 +- src/views/admin/manage/category-analytics.tpl | 14 ++- src/views/admin/manage/category.tpl | 82 ++++++++----- .../partials/categories/category-rows.tpl | 22 +++- .../admin/partials/categories/create.tpl | 6 +- .../admin/partials/categories/privileges.tpl | 57 ++++++--- .../partials/categories/select-category.tpl | 2 +- .../admin/partials/categories/setParent.tpl | 13 ++- 13 files changed, 309 insertions(+), 128 deletions(-) create mode 100644 public/language/en-GB/admin/manage/categories.json diff --git a/public/language/en-GB/admin/manage/categories.json b/public/language/en-GB/admin/manage/categories.json new file mode 100644 index 0000000000..8bd5d5bfcf --- /dev/null +++ b/public/language/en-GB/admin/manage/categories.json @@ -0,0 +1,68 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "parent-category-none": "(None)", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "privileges.description": "You can configure the access control privileges for this category in this section. Privileges can be granted on a per-user or a per-group basis. You can add a new user to this table by searching for them in the form below.", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-purge": "

    Do you really want to purge this category \"%1\"?

    Warning! All topics and posts in this category will be purged!

    Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

    ", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 was successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here..." +} \ No newline at end of file diff --git a/public/less/admin/manage/categories.less b/public/less/admin/manage/categories.less index 0f13af4deb..c0c0a544dc 100644 --- a/public/less/admin/manage/categories.less +++ b/public/less/admin/manage/categories.less @@ -103,6 +103,7 @@ div.categories { border-top: 0; text-transform: uppercase; font-size: 9px; + vertical-align: bottom; } .arrowed:after { diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index 217438e146..1c44a7e78d 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -87,7 +87,7 @@ url = url .replace(/\/\d+$/, '') .split('/').slice(0, 3).join('/') - .split('?')[0]; + .split('?')[0].replace(/(\/+$)|(^\/+)/, ''); // If index is requested, load the dashboard if (url === 'admin') { diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js index 1042c723e6..acb55b82e9 100644 --- a/public/src/admin/manage/categories.js +++ b/public/src/admin/manage/categories.js @@ -40,31 +40,29 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri templates.parse('admin/partials/categories/create', { categories: categories }, function (html) { - translator.translate(html, function (html) { - function submit() { - var formData = modal.find('form').serializeObject(); - formData.description = ''; - formData.icon = 'fa-comments'; - - Categories.create(formData); - modal.modal('hide'); - return false; - } + function submit() { + var formData = modal.find('form').serializeObject(); + formData.description = ''; + formData.icon = 'fa-comments'; + + Categories.create(formData); + modal.modal('hide'); + return false; + } - var modal = bootbox.dialog({ - title: 'Create a Category', - message: html, - buttons: { - save: { - label: 'Save', - className: 'btn-primary', - callback: submit - } + var modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.create]]', + message: html, + buttons: { + save: { + label: '[[global:save]]', + className: 'btn-primary', + callback: submit } - }); - - modal.find('form').on('submit', submit); + } }); + + modal.find('form').on('submit', submit); }); }); }; @@ -77,8 +75,8 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri app.alert({ alert_id: 'category_created', - title: 'Created', - message: 'Category successfully created!', + title: '[[admin/manage/categories:alert.created]]', + message: '[[admin/manage/categories:alert.create-success]]', type: 'success', timeout: 2000 }); @@ -91,10 +89,12 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri var container = $('.categories'); if (!categories || !categories.length) { - $('
    ') - .addClass('alert alert-info text-center') - .text('You have no active categories.') - .appendTo(container); + translator.translate('[[admin/manage/categories:alert.none-active]]', function (text) { + $('
    ') + .addClass('alert alert-info text-center') + .text(text) + .appendTo(container); + }); } else { sortables = {}; renderList(categories, container, 0); diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 32da35b007..351c7a624b 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -12,6 +12,45 @@ define('admin/manage/category', [ var modified_categories = {}; Category.init = function () { + var modified_categories = {}; + + function modified(el) { + var cid = $(el).parents('form').attr('data-cid'); + + if (cid) { + modified_categories[cid] = modified_categories[cid] || {}; + modified_categories[cid][$(el).attr('data-name')] = $(el).val(); + + app.flags = app.flags || {}; + app.flags._unsaved = true; + } + } + + function save(e) { + e.preventDefault(); + + if(Object.keys(modified_categories).length) { + socket.emit('admin.categories.update', modified_categories, function (err, results) { + if (err) { + return app.alertError(err.message); + } + + if (results && results.length) { + app.flags._unsaved = false; + app.alert({ + title: '[[admin/manage/categories:alert.updated]]', + message: translator.compile( + 'admin/manage/categories:alert.updated-success', + results.join(', ') + ), + type: 'success', + timeout: 2000 + }); + } + }); + modified_categories = {}; + } + } $('.blockclass, form.category select').each(function () { var $this = $(this); @@ -76,7 +115,10 @@ define('admin/manage/category', [ $('.purge').on('click', function (e) { e.preventDefault(); - bootbox.confirm('

    Do you really want to purge this category "' + $('form.category').find('input[data-name="name"]').val() + '"?

    Warning! All topics and posts in this category will be purged!

    Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you\'ll want to "disable" the category instead.

    ', function (confirm) { + bootbox.confirm(translator.compile( + 'admin/manage/categories:alert.confirm-purge', + $('form.category').find('input[data-name="name"]').val() + ), function (confirm) { if (!confirm) { return; } @@ -84,7 +126,7 @@ define('admin/manage/category', [ if (err) { return app.alertError(err.message); } - app.alertSuccess('Category purged!'); + app.alertSuccess('[[admin/manage/categories:alert.purge-success]]'); ajaxify.go('admin/manage/categories'); }); }); @@ -96,7 +138,7 @@ define('admin/manage/category', [ if (err) { return app.alertError(err.message); } - app.alertSuccess('Settings Copied!'); + app.alertSuccess('[[admin/manage/categories:alert.copy-success]]'); ajaxify.refresh(); }); }); @@ -108,7 +150,7 @@ define('admin/manage/category', [ var cid = inputEl.attr('data-cid'); uploader.show({ - title: 'Upload category image', + title: '[[admin/manage/categories:alert.upload-image]]', route: config.relative_path + '/api/admin/category/uploadpicture', params: {cid: cid} }, function (imageUrlOnServer) { @@ -201,7 +243,7 @@ define('admin/manage/category', [ if (member) { if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) { - bootbox.confirm('Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.', function (confirm) { + bootbox.confirm('[[admin/manage/categories:alert.confirm-moderate]]', function (confirm) { if (confirm) { Category.setPrivilege(member, privilege, state, checkboxEl); } else { @@ -292,35 +334,33 @@ define('admin/manage/category', [ templates.parse('partials/category_list', { categories: categories }, function (html) { - translator.translate(html, function (html) { - var modal = bootbox.dialog({ - message: html, - title: 'Set Parent Category' - }); + var modal = bootbox.dialog({ + message: html, + title: '[[admin/manage/categories:alert.set-parent-category]]' + }); - modal.find('li[data-cid]').on('click', function () { - var parentCid = $(this).attr('data-cid'), - payload = {}; + modal.find('li[data-cid]').on('click', function () { + var parentCid = $(this).attr('data-cid'), + payload = {}; - payload[ajaxify.data.category.cid] = { - parentCid: parentCid - }; + payload[ajaxify.data.category.cid] = { + parentCid: parentCid + }; - socket.emit('admin.categories.update', payload, function (err) { - if (err) { - return app.alertError(err.message); - } - var parent = categories.filter(function (category) { - return category && parseInt(category.cid, 10) === parseInt(parentCid, 10); - }); - parent = parent[0]; - - modal.modal('hide'); - $('button[data-action="removeParent"]').parent().removeClass('hide'); - $('button[data-action="setParent"]').addClass('hide'); - var buttonHtml = ' ' + parent.name; - $('button[data-action="changeParent"]').html(buttonHtml).parent().removeClass('hide'); + socket.emit('admin.categories.update', payload, function (err) { + if (err) { + return app.alertError(err.message); + } + var parent = categories.filter(function (category) { + return category && parseInt(category.cid, 10) === parseInt(parentCid, 10); }); + parent = parent[0]; + + modal.modal('hide'); + $('button[data-action="removeParent"]').parent().removeClass('hide'); + $('button[data-action="setParent"]').addClass('hide'); + var buttonHtml = ' ' + parent.name; + $('button[data-action="changeParent"]').html(buttonHtml).parent().removeClass('hide'); }); }); }); @@ -329,8 +369,8 @@ define('admin/manage/category', [ Category.addUserToPrivilegeTable = function () { var modal = bootbox.dialog({ - title: 'Find a User', - message: '', + title: '[[admin/manage/categories:alert.find-user]]', + message: '', show: true }); @@ -357,8 +397,8 @@ define('admin/manage/category', [ Category.addGroupToPrivilegeTable = function () { var modal = bootbox.dialog({ - title: 'Find a Group', - message: '', + title: '[[admin/manage/categories:alert.find-group]]', + message: '', show: true }); diff --git a/public/vendor/bootbox/wrapper.js b/public/vendor/bootbox/wrapper.js index bfc9457241..8e54e0e7db 100644 --- a/public/vendor/bootbox/wrapper.js +++ b/public/vendor/bootbox/wrapper.js @@ -30,15 +30,15 @@ require(['translator'], function (shim) { $elem = dialog.call(bootbox, options); - if (/\[\[[a-zA-Z0-9\-_.\/:]+\]\]/.test($elem[0].outerHTML)) { + if (/\[\[.+\]\]/.test($elem[0].outerHTML)) { nodes = descendantTextNodes($elem[0]); text = nodes.map(function (node) { return node.nodeValue; }).join(' || '); translator.translate(text).then(function (translated) { - translated.split(' || ').forEach(function (str, i) { - nodes[i].nodeValue = str; + translated.split(' || ').forEach(function (html, i) { + $(nodes[i]).replaceWith(html); }); if (show) { $elem.modal('show'); diff --git a/src/views/admin/manage/category-analytics.tpl b/src/views/admin/manage/category-analytics.tpl index eb02abd141..a582a3599e 100644 --- a/src/views/admin/manage/category-analytics.tpl +++ b/src/views/admin/manage/category-analytics.tpl @@ -1,6 +1,8 @@ - Back to Categories List + + [[admin/manage/categories:analytics.back]] + -

    Analytics for "{name}" category

    +

    [[admin/manage/categories:analytics.title, {name}]]


    @@ -12,7 +14,7 @@

    - +
    @@ -23,7 +25,7 @@

    - +
    @@ -36,7 +38,7 @@

    - +
    @@ -47,7 +49,7 @@

    - +
    \ No newline at end of file diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 7b2638b584..4d006ad191 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -1,8 +1,10 @@

    @@ -10,30 +12,40 @@
    - -
    + +
    - -
    + +
    - +
    - +
    - - @@ -42,19 +54,25 @@

    - +
    - +
    - +
    @@ -81,38 +99,53 @@
    - +
    - +

    - +
    - +
    - +
    - +

    - +
    - +
    @@ -120,12 +153,10 @@

    - You can configure the access control privileges for this category in this section. Privileges can be granted on a per-user or - a per-group basis. You can add a new user to this table by searching for them in the form below. + [[admin/manage/categories:privileges.description]]

    - Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting - these settings. + [[admin/manage/categories:privileges.warning]]


    @@ -139,4 +170,3 @@ - diff --git a/src/views/admin/partials/categories/category-rows.tpl b/src/views/admin/partials/categories/category-rows.tpl index 13e902fb99..1ceeefb9d4 100644 --- a/src/views/admin/partials/categories/category-rows.tpl +++ b/src/views/admin/partials/categories/category-rows.tpl @@ -4,7 +4,13 @@
    -
    +
    @@ -17,10 +23,18 @@
    - - Edit + + + + + [[admin/manage/categories:edit]] +
    diff --git a/src/views/admin/partials/categories/create.tpl b/src/views/admin/partials/categories/create.tpl index d4c551f1bf..436705b1d8 100644 --- a/src/views/admin/partials/categories/create.tpl +++ b/src/views/admin/partials/categories/create.tpl @@ -1,10 +1,10 @@
    - +
    - + diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/categories/privileges.tpl index 28b5f0849f..f15d207d6c 100644 --- a/src/views/admin/partials/categories/privileges.tpl +++ b/src/views/admin/partials/categories/privileges.tpl @@ -1,12 +1,18 @@
    hostpidnodejsonlinegitloaduptime[[admin/development/info:host]][[admin/development/info:pid]][[admin/development/info:nodejs]][[admin/development/info:online]][[admin/development/info:git]][[admin/development/info:load]][[admin/development/info:uptime]]
    {info.os.hostname}:{info.process.port} {info.process.pid} {info.process.version}{info.stats.onlineRegisteredCount} / {info.stats.onlineGuestCount} / {info.stats.socketCount} + {info.stats.onlineRegisteredCount} / + {info.stats.onlineGuestCount} / + {info.stats.socketCount} + {info.git.branch}@{info.git.hash} {info.os.load} {info.process.uptime}
    - - - + + + - + @@ -27,14 +33,18 @@ @@ -43,12 +53,18 @@
    Viewing PrivilegesPosting PrivilegesModeration Privileges + [[admin/manage/categories:privileges.section-viewing]] + + [[admin/manage/categories:privileges.section-posting]] + + [[admin/manage/categories:privileges.section-moderation]] +
    User[[admin/manage/categories:privileges.section-user]] {privileges.labels.users.name}
    - +
    - - No user-specific privileges in this category. + [[admin/manage/categories:privileges.no-users]] +
    - - - + + + - + @@ -57,7 +73,7 @@ @@ -68,16 +84,19 @@
    Viewing PrivilegesPosting PrivilegesModeration Privileges + [[admin/manage/categories:privileges.section-viewing]] + + [[admin/manage/categories:privileges.section-posting]] + + [[admin/manage/categories:privileges.section-moderation]] +
    Group[[admin/manage/categories:privileges.section-group]] {privileges.labels.groups.name}
    - + {privileges.groups.name}
    - - - + + +
    - If the registered-users group is granted a specific privilege, all other groups receive an - implicit privilege, even if they are not explicitly defined/checked. This implicit - privilege is shown to you because all users are part of the registered-users user group, - and so, privileges for additional groups need not be explicitly granted. + [[admin/manage/categories:privileges.inherit]]
    diff --git a/src/views/admin/partials/categories/select-category.tpl b/src/views/admin/partials/categories/select-category.tpl index 7e1f9f0d28..8c93286507 100644 --- a/src/views/admin/partials/categories/select-category.tpl +++ b/src/views/admin/partials/categories/select-category.tpl @@ -1,6 +1,6 @@
    - + + +
    @@ -27,7 +27,7 @@
    - + - - + +
    - + @@ -61,7 +61,7 @@
    - No flagged posts! + [[admin/manage/flags:none-flagged]]
    @@ -106,14 +106,16 @@
    - Posted in {posts.category.name}, • - Read More + [[posted-in, {posts.category.name}]], + • + [[admin/manage/flags:read-more]]
    - This post has been flagged {posts.flags} time(s): + + [[admin/manage/flags:flagged-x-times, {posts.flags}]]
      @@ -131,8 +133,12 @@
    - - + +
    @@ -141,7 +147,9 @@
    - +
    - +
    - +
    - +
    [[topic:flag_manage_history]]
    -
    [[topic:flag_manage_no_history]]
    +
    + [[topic:flag_manage_no_history]] +
      diff --git a/src/views/admin/manage/group.tpl b/src/views/admin/manage/group.tpl index a50fdba51b..f119f51914 100644 --- a/src/views/admin/manage/group.tpl +++ b/src/views/admin/manage/group.tpl @@ -3,27 +3,27 @@
      - + readonly/>
      - +
      - +
      -
      +

      - + {group.userTitle}
      @@ -31,7 +31,7 @@
      @@ -39,13 +39,14 @@
      @@ -55,7 +56,8 @@
      @@ -63,17 +65,18 @@
      - - + +
        @@ -81,7 +84,7 @@
        -

        Member List

        +

        [[admin/manage/groups:edit.members]]

        @@ -95,14 +98,16 @@
        -
        Groups Control Panel
        +
        [[admin/manage/groups:control-panel]]
        - +
        - +
        diff --git a/src/views/admin/manage/groups.tpl b/src/views/admin/manage/groups.tpl index 363a123ec1..c2b0ca2f32 100644 --- a/src/views/admin/manage/groups.tpl +++ b/src/views/admin/manage/groups.tpl @@ -4,20 +4,22 @@
        - - + +
        Group NameGroup Description[[admin/manage/groups:name]][[admin/manage/groups:description]]
        {groups.displayName} - System Group + [[admin/manage/groups:system]]
        - Edit + + [[admin/manage/groups:edit]] + @@ -36,7 +38,7 @@
        - +
        @@ -47,24 +49,28 @@
        diff --git a/src/views/admin/manage/ip-blacklist.tpl b/src/views/admin/manage/ip-blacklist.tpl index 26170b286e..23f54ff04a 100644 --- a/src/views/admin/manage/ip-blacklist.tpl +++ b/src/views/admin/manage/ip-blacklist.tpl @@ -1,12 +1,10 @@

        - Configure your IP blacklist here. + [[admin/manage/ip-blacklist:lead]]

        - Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs - is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and - they will be prevented from logging in to or registering a new account. + [[admin/manage/ip-blacklist:description]]

        @@ -15,21 +13,24 @@
        -
        Active Rules
        +
        [[admin/manage/ip-blacklist:active-rules]]
        - - + +
        -
        Syntax Hints
        +
        [[admin/manage/ip-blacklist:hints]]

        - Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. - 192.168.100.0/22). + [[admin/manage/ip-blacklist:hint-1]]

        - You can add in comments by starting lines with the # symbol. + [[admin/manage/ip-blacklist:hint-2]]

        diff --git a/src/views/admin/manage/registration.tpl b/src/views/admin/manage/registration.tpl index 8b28b2ef0f..f8504fb31d 100644 --- a/src/views/admin/manage/registration.tpl +++ b/src/views/admin/manage/registration.tpl @@ -1,21 +1,19 @@
        - Queue + [[admin/manage/registration:queue]]

        - There are no users in the registration queue.
        - To enable this feature, go to Settings → User → User Registration and set - Registration Type to "Admin Approval". + [[admin/manage/registration:description, {config.relative_path}/admin/settings/user]]

        - - - - + + + + @@ -25,7 +23,7 @@
        NameEmail[[admin/manage/registration:list.name]][[admin/manage/registration:list.email]]
        - + @@ -33,7 +31,7 @@ - + @@ -41,7 +39,7 @@