diff --git a/.jshintrc b/.jshintrc
index 3a7e093b8f..172f05cea8 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -83,5 +83,7 @@
     "white" : false, // true: Check against strict whitespace and indentation rules
 
     // Custom Globals
-    "globals" : {} // additional predefined global variables
+    "globals" : {
+        "Promise": true
+    } // additional predefined global variables
 }
\ No newline at end of file
diff --git a/package.json b/package.json
index 839a817c27..c0114b1978 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
     "request": "^2.44.0",
     "rimraf": "~2.5.0",
     "rss": "^1.0.0",
+    "sanitize-html": "^1.13.0",
     "semver": "^5.1.0",
     "serve-favicon": "^2.1.5",
     "sitemap": "^1.4.0",
diff --git a/public/language/en-GB/admin/appearance/skins.json b/public/language/en-GB/admin/appearance/skins.json
new file mode 100644
index 0000000000..4db6fbdd8a
--- /dev/null
+++ b/public/language/en-GB/admin/appearance/skins.json
@@ -0,0 +1,9 @@
+{
+	"loading": "Loading Skins...",
+	"homepage": "Homepage",
+	"select-skin": "Select Skin",
+	"current-skin": "Current Skin",
+	"skin-updated": "Skin Updated",
+	"applied-success": "%1 skin was succesfully applied",
+	"revert-success": "Skin reverted to base colours"
+}
\ No newline at end of file
diff --git a/public/language/en-GB/admin/appearance/themes.json b/public/language/en-GB/admin/appearance/themes.json
new file mode 100644
index 0000000000..3148a01337
--- /dev/null
+++ b/public/language/en-GB/admin/appearance/themes.json
@@ -0,0 +1,11 @@
+{
+    "checking-for-installed": "Checking for installed themes...",
+    "homepage": "Homepage",
+    "select-theme": "Select Theme",
+    "current-theme": "Current Theme",
+    "no-themes": "No installed themes found",
+    "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?",
+    "theme-changed": "Theme Changed",
+    "revert-success": "You have successfully reverted your NodeBB back to it's default theme.",
+    "restart-to-activate": "Please restart your NodeBB to fully activate this theme"
+}
\ No newline at end of file
diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less
index 0ceeb050e5..aa5efff256 100644
--- a/public/less/admin/admin.less
+++ b/public/less/admin/admin.less
@@ -29,6 +29,7 @@
 @import "./modules/selectable";
 @import "./modules/snackbar";
 @import "./modules/nprogress";
+@import "./modules/search";
 
 body {
 	overflow-y: scroll;
diff --git a/public/less/admin/modules/search.less b/public/less/admin/modules/search.less
new file mode 100644
index 0000000000..d2286005bf
--- /dev/null
+++ b/public/less/admin/modules/search.less
@@ -0,0 +1,45 @@
+#acp-search {
+    .dropdown-menu {
+        max-height: 75vh;
+        overflow-y: auto;
+
+        > li > a {
+            &.focus {
+                &:extend(.dropdown-menu>li>a:focus);
+            }
+            &:focus {
+                outline: none;
+            }
+        }
+    }
+
+    .state-start-typing {
+        .keep-typing, .search-forum, .no-results {
+            display: none;
+        }
+    }
+
+    .state-keep-typing {
+        .start-typing, .search-forum, .no-results {
+            display: none;
+        }
+    }
+
+    .state-no-results {
+        .keep-typing, .start-typing {
+            display: none;
+        }
+    }
+
+    .state-yes-results {
+        .keep-typing, .start-typing, .no-results {
+            display: none;
+        }
+    }
+
+    .search-disabled {
+        .search-forum {
+            display: none;
+        }
+    }
+}
\ No newline at end of file
diff --git a/public/src/admin/appearance/skins.js b/public/src/admin/appearance/skins.js
index 7583dc952d..35ad5a2289 100644
--- a/public/src/admin/appearance/skins.js
+++ b/public/src/admin/appearance/skins.js
@@ -1,7 +1,7 @@
 "use strict";
 /* global define, app, socket, templates */
 
-define('admin/appearance/skins', function () {
+define('admin/appearance/skins', ['translator'], function (translator) {
 	var Skins = {};
 	
 	Skins.init = function () {
@@ -40,8 +40,8 @@ define('admin/appearance/skins', function () {
 					app.alert({
 						alert_id: 'admin:theme',
 						type: 'info',
-						title: 'Skin Updated',
-						message: themeId ? (themeId + ' skin was successfully applied') : 'Skin reverted to base colours',
+						title: '[[admin/appearance/skins:skin-updated]]',
+						message: themeId ? ('[[admin/appearance/skins:applied-success, ' + themeId + ']]') : '[[admin/appearance/skins:revert-success]]',
 						timeout: 5000
 					});
 				});
@@ -67,40 +67,48 @@ define('admin/appearance/skins', function () {
 			}),
 			showRevert: true
 		}, function (html) {
-			themeContainer.html(html);
+			translator.translate(html, function (html) {
+				themeContainer.html(html);
 
-			if (config['theme:src']) {
-				var skin = config['theme:src']
+				if (config['theme:src']) {
+					var skin = config['theme:src']
 					.match(/latest\/(\S+)\/bootstrap.min.css/)[1]
 					.replace(/(^|\s)([a-z])/g , function (m,p1,p2) {return p1 + p2.toUpperCase();});
 
-				highlightSelectedTheme(skin);
-			}
+					highlightSelectedTheme(skin);
+				}
+			});
 		});
 	};
 
 	function highlightSelectedTheme(themeId) {
-		$('[data-theme]')
-			.removeClass('selected')
-			.find('[data-action="use"]').each(function () {
-				if ($(this).parents('[data-theme]').attr('data-theme')) {
-					$(this)
-						.html('Select Skin')
-						.removeClass('btn-success')
-						.addClass('btn-primary');
-				}
-			});
+		translator.translate('[[admin/appearance/skins:select-skin]]  ||  [[admin/appearance/skins:current-skin]]', function (text) {
+			text = text.split('  ||  ');
+			var select = text[0];
+			var current = text[1];
+
+			$('[data-theme]')
+				.removeClass('selected')
+				.find('[data-action="use"]').each(function () {
+					if ($(this).parents('[data-theme]').attr('data-theme')) {
+						$(this)
+							.html(select)
+							.removeClass('btn-success')
+							.addClass('btn-primary');
+					}
+				});
 
-		if (!themeId) {
-			return;
-		}
+			if (!themeId) {
+				return;
+			}
 
-		$('[data-theme="' + themeId + '"]')
-			.addClass('selected')
-			.find('[data-action="use"]')
-				.html('Current Skin')
-				.removeClass('btn-primary')
-				.addClass('btn-success');
+			$('[data-theme="' + themeId + '"]')
+				.addClass('selected')
+				.find('[data-action="use"]')
+					.html(current)
+					.removeClass('btn-primary')
+					.addClass('btn-success');
+		});
 	}
 
 	return Skins;
diff --git a/public/src/admin/appearance/themes.js b/public/src/admin/appearance/themes.js
index 835dcda654..0c71baa4c4 100644
--- a/public/src/admin/appearance/themes.js
+++ b/public/src/admin/appearance/themes.js
@@ -1,7 +1,7 @@
 "use strict";
 /* global define, app, socket, bootbox, templates, config */
 
-define('admin/appearance/themes', function () {
+define('admin/appearance/themes', ['translator'], function (translator) {
 	var Themes = {};
 	
 	Themes.init = function () {
@@ -28,8 +28,8 @@ define('admin/appearance/themes', function () {
 					app.alert({
 						alert_id: 'admin:theme',
 						type: 'info',
-						title: 'Theme Changed',
-						message: 'Please restart your NodeBB to fully activate this theme',
+						title: '[[admin/appearance/themes:theme-changed]]',
+						message: '[[admin/appearance/themes:restart-to-activate]]',
 						timeout: 5000,
 						clickfn: function () {
 							socket.emit('admin.restart');
@@ -38,27 +38,29 @@ define('admin/appearance/themes', function () {
 				});
 			}
 		});
-
-		$('#revert_theme').on('click', function () {
-			bootbox.confirm('Are you sure you wish to restore the default NodeBB theme?', function (confirm) {
-				if (confirm) {
-					socket.emit('admin.themes.set', {
-						type: 'local',
-						id: 'nodebb-theme-persona'
-					}, function (err) {
-						if (err) {
-							return app.alertError(err.message);
-						}
-						highlightSelectedTheme('nodebb-theme-persona');
-						app.alert({
-							alert_id: 'admin:theme',
-							type: 'success',
-							title: 'Theme Changed',
-							message: 'You have successfully reverted your NodeBB back to it\'s default theme.',
-							timeout: 3500
+		
+		translator.translate('[[admin/appearance/themes:revert-confirm]]', function (revert) {
+			$('#revert_theme').on('click', function () {
+				bootbox.confirm(revert, function (confirm) {
+					if (confirm) {
+						socket.emit('admin.themes.set', {
+							type: 'local',
+							id: 'nodebb-theme-persona'
+						}, function (err) {
+							if (err) {
+								return app.alertError(err.message);
+							}
+							highlightSelectedTheme('nodebb-theme-persona');
+							app.alert({
+								alert_id: 'admin:theme',
+								type: 'success',
+								title: '[[admin/appearance/themes:theme-changed]]',
+								message: '[[admin/appearance/themes:revert-success]]',
+								timeout: 3500
+							});
 						});
-					});
-				}
+					}
+				});
 			});
 		});
 
@@ -70,17 +72,17 @@ define('admin/appearance/themes', function () {
 			var instListEl = $('#installed_themes');
 
 			if (!themes.length) {
-				instListEl.append($('<li/ >').addClass('no-themes').html('No installed themes found'));
+				translator.translate('[[admin/appearance/themes:no-themes]]', function (text) {
+					instListEl.append($('<li/ >').addClass('no-themes').html(text));
+				});
 				return;
 			} else {
 				templates.parse('admin/partials/theme_list', {
 					themes: themes
 				}, function (html) {
-					require(['translator'], function (translator) {
-						translator.translate(html, function (html) {
-							instListEl.html(html);
-							highlightSelectedTheme(config['theme:id']);
-						});
+					translator.translate(html, function (html) {
+						instListEl.html(html);
+						highlightSelectedTheme(config['theme:id']);
 					});
 				});
 			}
@@ -88,19 +90,25 @@ define('admin/appearance/themes', function () {
 	};
 
 	function highlightSelectedTheme(themeId) {
-		$('[data-theme]')
-			.removeClass('selected')
-			.find('[data-action="use"]')
-				.html('Select Theme')
-				.removeClass('btn-success')
-				.addClass('btn-primary');
+		translator.translate('[[admin/appearance/themes:select-theme]]  ||  [[admin/appearance/themes:current-theme]]', function (text) {
+			text = text.split('  ||  ');
+			var select = text[0];
+			var current = text[1];
 
-		$('[data-theme="' + themeId + '"]')
-			.addClass('selected')
-			.find('[data-action="use"]')
-				.html('Current Theme')
-				.removeClass('btn-primary')
-				.addClass('btn-success');
+			$('[data-theme]')
+				.removeClass('selected')
+				.find('[data-action="use"]')
+					.html(select)
+					.removeClass('btn-success')
+					.addClass('btn-primary');
+
+			$('[data-theme="' + themeId + '"]')
+				.addClass('selected')
+				.find('[data-action="use"]')
+					.html(current)
+					.removeClass('btn-primary')
+					.addClass('btn-success');
+		});
 	}
 
 	return Themes;
diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js
index f64a2490e7..4f46898b8e 100644
--- a/public/src/admin/modules/search.js
+++ b/public/src/admin/modules/search.js
@@ -1,114 +1,148 @@
 "use strict";
-/*globals define, admin, ajaxify, RELATIVE_PATH*/
+/* globals socket, app, define, ajaxify, config */
 
-define(function () {
-	var search = {},
-		searchIndex;
+define('admin/modules/search', ['mousetrap'], function (mousetrap) {
+	var search = {};
 
-	search.init = function () {
-		$.getJSON(RELATIVE_PATH + '/templates/indexed.json', function (data) {
-			searchIndex = data;
-			for (var file in searchIndex) {
-				if (searchIndex.hasOwnProperty(file)) {
-					searchIndex[file] = searchIndex[file].replace(/<img/g, '<none'); // can't think of a better solution, see #2153
-					searchIndex[file] = $('<div class="search-container">' + searchIndex[file] + '</div>');
-					searchIndex[file].find('script').remove();
-
-					searchIndex[file] = searchIndex[file].text().toLowerCase().replace(/[ |\r|\n]+/g, ' ');
-				}
-			}
+	function nsToTitle(namespace) {
+		return namespace.replace('admin/', '').split('/').map(function (str) {
+			return str[0].toUpperCase() + str.slice(1);
+		}).join(' > ');
+	}
 
-			delete searchIndex['/admin/header.tpl'];
-			delete searchIndex['/admin/footer.tpl'];
+	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 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
+				// and wrap the match in a `.search-match` element
+				.replace(
+					new RegExp('^[\\s\\S]*?(.{0,25})(' + term + ')(.{0,25})[\\s\\S]*?$', 'gmi'),
+					'...$1<span class="search-match">$2</span>$3...<br>'
+				)
+				// collapse whitespace
+				.replace(/(?:\n ?)+/g, '\n');
+
+			return '<li role="presentation" class="result">' +
+				'<a role= "menuitem" href= "' + config.relative_path + '/' + namespace + '" >' +
+					title +
+					'<br>' +
+					'<small><code>' +
+						results +
+					'</small></code>' +
+				'</a>' +
+			'</li>';
+		}).join('');
+		return html;
+	}
 
-			setupACPSearch();
+	search.init = function () {
+		socket.emit('admin.getSearchDict', {}, function (err, dict) {
+			if (err) {
+				app.alertError(err);
+				throw err;
+			}
+			setupACPSearch(dict);
 		});
 	};
 
-	function setupACPSearch() {
-		var menu = $('#acp-search .dropdown-menu'),
-			routes = [],
-			input = $('#acp-search input'),
-			firstResult = null;
+	function setupACPSearch(dict) {
+		var dropdown = $('#acp-search .dropdown');
+		var menu = $('#acp-search .dropdown-menu');
+		var input = $('#acp-search input');
+
+		if (!config.searchEnabled) {
+			menu.addClass('search-disabled');
+		}
 
 		input.on('keyup', function () {
-			$('#acp-search .dropdown').addClass('open');
+			dropdown.addClass('open');
 		});
 
 		$('#acp-search').parents('form').on('submit', function (ev) {
-			var input = $(this).find('input'),
-				href = firstResult ? firstResult : RELATIVE_PATH + '/search/' + input.val();
+			var selected = menu.find('li.result > a.focus').attr('href');
+			if (!selected.length) {
+				selected = menu.find('li.result > a').first().attr('href');
+			}
+			var href = selected ? selected : config.relative_path + '/search/' + input.val();
 
 			ajaxify.go(href.replace(/^\//, ''));
 
 			setTimeout(function () {
-				$('#acp-search .dropdown').removeClass('open');
-				$(input).blur();
+				dropdown.removeClass('open');
+				input.blur();
 			}, 150);
 
 			ev.preventDefault();
 			return false;
 		});
 
-		$('#main-menu a').each(function (idx, link) {
-			routes.push($(link).attr('href'));
+		mousetrap(input[0]).bind(['up', 'down'], function (ev, key) {
+			var next;
+			if (key === 'up') {
+				next = menu.find('li.result > a.focus').removeClass('focus').parent().prev('.result').children();
+				if (!next.length) {
+					next = menu.find('li.result > a').last();
+				}
+				next.addClass('focus');
+				if (menu[0].getBoundingClientRect().top > next[0].getBoundingClientRect().top) {
+					next[0].scrollIntoView(true);
+				}
+			} else if (key === 'down') {
+				next = menu.find('li.result > a.focus').removeClass('focus').parent().next('.result').children();
+				if (!next.length) {
+					next = menu.find('li.result > a').first();
+				}
+				next.addClass('focus');
+				if (menu[0].getBoundingClientRect().bottom < next[0].getBoundingClientRect().bottom) {
+					next[0].scrollIntoView(false);
+				}
+			}
+
+			ev.preventDefault();
 		});
 
+		var prevValue;
+
 		input.on('keyup focus', function () {
-			var $input = $(this),
-				value = $input.val().toLowerCase(),
-				menuItems = $('#acp-search .dropdown-menu').html('');
+			var value = input.val().toLowerCase();
 
-			function toUpperCase(txt) {
-				return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+			if (value === prevValue) {
+				return;
 			}
+			prevValue = value;
 
-			firstResult = null;
-
-			if (value.length >= 3) {
-				for (var file in searchIndex) {
-					if (searchIndex.hasOwnProperty(file)) {
-						var position = searchIndex[file].indexOf(value);
-
-						if (position !== -1) {
-							var href = file.replace('.tpl', ''),
-								title = href.replace(/^\/admin\//, '').split('/'),
-								description = searchIndex[file].substring(Math.max(0, position - 25), Math.min(searchIndex[file].length - 1, position + 25))
-									.replace(value, '<span class="search-match">' + value + '</span>');
-
-							for (var t in title) {
-								if (title.hasOwnProperty(t)) {
-									title[t] = title[t]
-										.replace('-', ' ')
-										.replace(/\w\S*/g, toUpperCase);
-								}
-							}
-
-							title = title.join(' > ');
-							href = RELATIVE_PATH + href;
-							firstResult = firstResult ? firstResult : href;
-
-							if ($.inArray(href, routes) !== -1) {
-								menuItems.append('<li role="presentation"><a role="menuitem" href="' + href + '">' + title + '<br /><small><code>...' + description + '...</code></small></a></li>');
-							}
-						}
-					}
-				}
+			menu.children('.result').remove();
 
-				if (menuItems.html() === '') {
-					menuItems.append('<li role="presentation"><a role="menuitem" href="#">No results...</a></li>');
-				}
-			}
+			var len = value.length;
+			var results;
 
-			if (value.length > 0) {
-				if (config.searchEnabled) {
-					menuItems.append('<li role="presentation" class="divider"></li>');
-					menuItems.append('<li role="presentation"><a role="menuitem" target="_top" href="' + RELATIVE_PATH + '/search/' + value + '">Search the forum for <strong>' + value + '</strong></a></li>');
-				} else if (value.length < 3) {
-					menuItems.append('<li role="presentation"><a role="menuitem" href="#">Type more to see results...</a></li>');
-				}
+			menu.toggleClass('state-start-typing', len === 0);
+			menu.toggleClass('state-keep-typing', len > 0 && len < 3);
+			
+			if (len >= 3) {
+				menu.prepend(find(dict, value));
+
+				results = menu.children('.result').length;
+
+				menu.toggleClass('state-no-results', !results);
+				menu.toggleClass('state-yes-results', !!results);
+
+				menu.find('.search-forum')
+					.not('.divider')
+					.find('a')
+					.attr('href', config.relative_path + '/search/' + value)
+					.find('strong')
+					.html(value);
 			} else {
-				menuItems.append('<li role="presentation"><a role="menuitem" href="#">Start typing to see results...</a></li>');
+				menu.removeClass('state-no-results state-yes-results');
 			}
 		});
 	}
diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js
index 0771ab56a5..a876dba44c 100644
--- a/public/src/modules/translator.js
+++ b/public/src/modules/translator.js
@@ -3,7 +3,7 @@
 (function (factory) {
 	'use strict';
 	function loadClient(language, namespace) {
-		return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + namespace));
+		return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + encodeURIComponent(namespace)));
 	}
 	if (typeof define === 'function' && define.amd) {
 		// AMD. Register as a named module
@@ -339,6 +339,68 @@
 
 		Translator.moduleFactories = {};
 
+		/**
+		 * Remove the translator patterns from text
+		 * @param {string} text
+		 * @returns {string}
+		 */
+		Translator.removePatterns = function removePatterns(text) {
+			var len = text.length;
+			var cursor = 0;
+			var lastBreak = 0;
+			var level = 0;
+			var out = '';
+			var sub;
+
+			while (cursor < len) {
+				sub = text.slice(cursor, cursor + 2);
+				if (sub === '[[') {
+					if (level === 0) {
+						out += text.slice(lastBreak, cursor);
+					}
+					level += 1;
+					cursor += 2;
+				} else if (sub === ']]') {
+					level -= 1;
+					cursor += 2;
+					if (level === 0) {
+						lastBreak = cursor;
+					}
+				} else {
+					cursor += 1;
+				}
+			}
+			out += text.slice(lastBreak, cursor);
+			return out;
+		};
+
+		/**
+		 * Escape translator patterns in text
+		 * @param {string} text
+		 * @returns {string}
+		 */
+		Translator.escape = function escape(text) {
+			return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text;
+		};
+
+		/**
+		 * Unescape escaped translator patterns in text
+		 * @param {string} text
+		 * @returns {string}
+		 */
+		Translator.unescape = function unescape(text) {
+			return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text;
+		};
+
+		/**
+		 * Construct a translator pattern
+		 */
+		Translator.compile = function compile() {
+			var args = Array.prototype.slice.call(arguments, 0);
+
+			return '[[' + args.join(', ') + ']]';
+		};
+
 		return Translator;
 	}());
 
@@ -348,12 +410,16 @@
 		 */
 		Translator: Translator,
 
+		compile: Translator.compile,
+		escape: Translator.escape,
+		unescape: Translator.unescape,
+		getLanguage: Translator.getLanguage,
+
 		/**
 		 * Legacy translator function for backwards compatibility
 		 */
 		translate: function translate(text, language, callback) {
-			// console.warn('[translator] `translator.translate(text, [lang, ]callback)` is deprecated. ' +
-			//   'Use the `translator.Translator` class instead.');
+			// TODO: deprecate?
 
 			var cb = callback;
 			var lang = language;
@@ -373,31 +439,6 @@
 			});
 		},
 
-		/**
-		 * Construct a translator pattern
-		 * @param {string} name - Translation name
-		 * @param {string[]} args - Optional arguments for the pattern
-		 */
-		compile: function compile() {
-			var args = Array.prototype.slice.call(arguments, 0);
-
-			return '[[' + args.join(', ') + ']]';
-		},
-
-		/**
-		 * Escape translation patterns from text
-		 */
-		escape: function escape(text) {
-			return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text;
-		},
-
-		/**
-		 * Unescape translation patterns from text
-		 */
-		unescape: function unescape(text) {
-			return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text;
-		},
-
 		/**
 		 * Add translations to the cache
 		 */
@@ -422,11 +463,6 @@
 			adaptor.getTranslations(language, namespace, callback);
 		},
 
-		/**
-		 * Get the language of the current environment, falling back to defaults
-		 */
-		getLanguage: Translator.getLanguage,
-
 		toggleTimeagoShorthand: function toggleTimeagoShorthand() {
 			var tmp = assign({}, jQuery.timeago.settings.strings);
 			jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
diff --git a/src/admin/search.js b/src/admin/search.js
new file mode 100644
index 0000000000..8f567071bf
--- /dev/null
+++ b/src/admin/search.js
@@ -0,0 +1,183 @@
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var async = require('async');
+var sanitizeHTML = require('sanitize-html');
+
+var languages = require('../languages');
+var utils = require('../../public/src/utils');
+var Translator = require('../../public/src/modules/translator').Translator;
+
+function filterDirectories(directories) {
+	return directories.map(function (dir) {
+		// get the relative path
+		return dir.replace(/^.*(admin.*?).tpl$/, '$1');
+	}).filter(function (dir) {
+		// exclude partials
+		// only include subpaths
+		// exclude category.tpl, group.tpl, category-analytics.tpl
+		return !dir.includes('/partials/') &&
+			/\/.*\//.test(dir) &&
+			!/category|group|category\-analytics$/.test(dir);
+	});
+}
+
+function getAdminNamespaces(callback) {
+	utils.walk(path.resolve(__dirname, '../../public/templates/admin'), function (err, directories) {
+		if (err) {
+			return callback(err);
+		}
+
+		callback(null, filterDirectories(directories));
+	});
+}
+
+function sanitize(html) {
+	// reduce the template to just meaningful text
+	// remove all tags and strip out scripts, etc completely
+	return sanitizeHTML(html, {
+		allowedTags: [],
+		allowedAttributes: [],
+	});
+}
+
+function simplify(translations) {
+	return translations
+		// remove all mustaches
+		.replace(/(?:\{{1,2}[^\}]*?\}{1,2})/g, '')
+		// collapse whitespace
+		.replace(/(?:[ \t]*[\n\r]+[ \t]*)+/g, '\n')
+		.replace(/[\t ]+/g, ' ');
+}
+
+function nsToTitle(namespace) {
+	return namespace.replace('admin/', '').split('/').map(function (str) {
+		return str[0].toUpperCase() + str.slice(1);
+	}).join(' > ');
+}
+
+var fallbackCacheInProgress = {};
+var fallbackCache = {};
+
+function initFallback(namespace, callback) {
+	fs.readFile(path.resolve(__dirname, '../../public/templates/', namespace + '.tpl'), function (err, file) {
+		if (err) {
+			return callback(err);
+		}
+
+		var template = file.toString();
+
+		var translations = sanitize(template);
+		translations = Translator.removePatterns(translations);
+		translations = simplify(translations);
+		translations += '\n' + nsToTitle(namespace);
+
+		callback(null, {
+			namespace: namespace,
+			translations: translations,
+		});
+	});
+}
+
+function fallback(namespace, callback) {
+	if (fallbackCache[namespace]) {
+		return callback(null, fallbackCache[namespace]);
+	}
+	if (fallbackCacheInProgress[namespace]) {
+		return fallbackCacheInProgress[namespace].push(callback);
+	}
+
+	fallbackCacheInProgress[namespace] = [function (err, params) {
+		if (err) {
+			return callback(err);
+		}
+
+		callback(null, params);
+	}];
+	initFallback(namespace, function (err, params) {
+		fallbackCacheInProgress[namespace].forEach(function (fn) {
+			fn(err, params);
+		});
+		fallbackCacheInProgress[namespace] = null;
+		fallbackCache[namespace] = params;
+	});
+}
+
+function initDict(language, callback) {
+	getAdminNamespaces(function (err, namespaces) {
+		if (err) {
+			return callback(err);
+		}
+
+		async.map(namespaces, function (namespace, cb) {
+			async.waterfall([
+				function (next) {
+					languages.get(language, namespace, next);
+				},
+				function (translations, next) {
+					if (!translations || !Object.keys(translations).length) {
+						return next(Error('No translations for ' + language + '/' + namespace));
+					}
+
+					// join all translations into one string separated by newlines
+					var str = Object.keys(translations).map(function (key) {
+						return translations[key];
+					}).join('\n');
+
+					next(null, {
+						namespace: namespace,
+						translations: str,
+					});
+				}
+			], function (err, params) {
+				if (err) {
+					return fallback(namespace, function (err, params) {
+						if (err) {
+							return cb({
+								namespace: namespace,
+								translations: '',
+							});
+						}
+
+						cb(null, params);
+					});
+				}
+
+				cb(null, params);
+			});
+		}, callback);
+	});
+}
+
+var cacheInProgress = {};
+var cache = {};
+
+function getDictionary(language, callback) {
+	if (cache[language]) {
+		return callback(null, cache[language]);
+	}
+	if (cacheInProgress[language]) {
+		return cacheInProgress[language].push(callback);
+	}
+
+	cacheInProgress[language] = [function (err, params) {
+		if (err) {
+			return callback(err);
+		}
+
+		callback(null, params);
+	}];
+	initDict(language, function (err, params) {
+		cacheInProgress[language].forEach(function (fn) {
+			fn(err, params);
+		});
+		cacheInProgress[language] = null;
+		cache[language] = params;
+	});
+}
+
+module.exports.getDictionary = getDictionary;
+module.exports.filterDirectories = filterDirectories;
+module.exports.simplify = simplify;
+module.exports.sanitize = sanitize;
diff --git a/src/languages.js b/src/languages.js
index 86563628b2..f3b9aa5743 100644
--- a/src/languages.js
+++ b/src/languages.js
@@ -75,7 +75,7 @@ Languages.list = function (callback) {
 
 				fs.readFile(configPath, function (err, stream) {
 					if (err) {
-						next();
+						return next(err);
 					}
 					languages.push(JSON.parse(stream.toString()));
 					next();
diff --git a/src/meta/templates.js b/src/meta/templates.js
index 4e7f934624..fd5c1f71e1 100644
--- a/src/meta/templates.js
+++ b/src/meta/templates.js
@@ -12,7 +12,6 @@ var plugins = require('../plugins');
 var utils = require('../../public/src/utils');
 
 var Templates = {};
-var searchIndex = {};
 
 Templates.compile = function (callback) {
 	callback = callback || function () {};
@@ -131,10 +130,6 @@ function compile(callback) {
 				}
 			}
 
-			if (relativePath.match(/^\/admin\/[\s\S]*?/)) {
-				addIndex(relativePath, file);
-			}
-
 			mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/')));
 			fs.writeFile(path.join(viewsPath, relativePath), file, next);
 		}, function (err) {
@@ -143,25 +138,11 @@ function compile(callback) {
 				return callback(err);
 			}
 
-			compileIndex(viewsPath, function (err) {
-				if (err) {
-					return callback(err);
-				}
-				winston.verbose('[meta/templates] Successfully compiled templates.');
+			winston.verbose('[meta/templates] Successfully compiled templates.');
 
-				callback();
-			});
+			callback();
 		});
 	});
 }
 
-
-function addIndex(path, file) {
-	searchIndex[path] = file;
-}
-
-function compileIndex(viewsPath, callback) {
-	fs.writeFile(path.join(viewsPath, '/indexed.json'), JSON.stringify(searchIndex), callback);
-}
-
 module.exports = Templates;
\ No newline at end of file
diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js
index c3220ed094..141d567f11 100644
--- a/src/socket.io/admin.js
+++ b/src/socket.io/admin.js
@@ -15,6 +15,7 @@ var emailer = require('../emailer');
 var db = require('../database');
 var analytics = require('../analytics');
 var index = require('./index');
+var getAdminSearchDict = require('../admin/search').getDictionary;
 
 var SocketAdmin = {
 	user: require('./admin/user'),
@@ -277,5 +278,15 @@ SocketAdmin.deleteAllEvents = function (socket, data, callback) {
 	events.deleteAll(callback);
 };
 
+SocketAdmin.getSearchDict = function (socket, data, callback) {
+	user.getSettings(socket.uid, function (err, settings) {
+		if (err) {
+			return callback(err);
+		}
+		var lang = settings.userLang || meta.config.defaultLang || 'en-GB';
+		getAdminSearchDict(lang, callback);
+	});
+};
+
 
 module.exports = SocketAdmin;
diff --git a/src/views/admin/appearance/skins.tpl b/src/views/admin/appearance/skins.tpl
index 0c1b543a47..c5d1355f08 100644
--- a/src/views/admin/appearance/skins.tpl
+++ b/src/views/admin/appearance/skins.tpl
@@ -1,6 +1,6 @@
 <div id="skins" class="row skins">
 	<div class="directory row" id="bootstrap_themes">
-		<i class="fa fa-refresh fa-spin"></i> Loading Skins
+		<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
 	</div>
 
 	<div data-type="bootswatch" data-theme="" data-css="">
diff --git a/src/views/admin/appearance/themes.tpl b/src/views/admin/appearance/themes.tpl
index e67c6a2755..8203c84e8b 100644
--- a/src/views/admin/appearance/themes.tpl
+++ b/src/views/admin/appearance/themes.tpl
@@ -1,6 +1,6 @@
 <div id="themes" class="themes">
 	<div class="directory row" id="installed_themes">
-		<i class="fa fa-refresh fa-spin"></i> Checking for installed themes...
+		<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/themes:checking-for-installed]]
 	</div>
 </div>
 
diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl
index 0fea719e46..9bc0c48d7e 100644
--- a/src/views/admin/partials/menu.tpl
+++ b/src/views/admin/partials/menu.tpl
@@ -149,7 +149,23 @@
 				<div class="" id="acp-search" >
 					<div class="dropdown">
 						<input type="text" data-toggle="dropdown" class="form-control" placeholder="Search...">
-						<ul class="dropdown-menu dropdown-menu-right" role="menu"></ul>
+						<ul class="dropdown-menu dropdown-menu-right state-start-typing" role="menu">
+							<li role="presentation" class="no-results">
+								<a>No results...</a>
+							</li>
+							<li role="presentation" class="divider search-forum"></li>
+							<li role="presentation" class="search-forum">
+								<a role="menuitem" target="_top" href="#">
+									Search the forum for <strong></strong>
+								</a>
+							</li>
+							<li role="presentation" class="keep-typing">
+								<a>Type more to see results...</a>
+							</li>
+							<li role="presentation" class="start-typing">
+								<a>Start typing to see results...</a>
+							</li>
+						</ul>
 					</div>
 				</div>
 			</form>
diff --git a/src/views/admin/partials/theme_list.tpl b/src/views/admin/partials/theme_list.tpl
index 1517eb5c9e..26a226b803 100644
--- a/src/views/admin/partials/theme_list.tpl
+++ b/src/views/admin/partials/theme_list.tpl
@@ -10,13 +10,13 @@
 
 			<!-- IF themes.url -->
 			<p>
-				<a href="{themes.url}" target="_blank">Homepage</a>
+				<a href="{themes.url}" target="_blank">[[admin/appearance/themes:homepage]]</a>
 			</p>
 			<!-- ENDIF themes.url -->
 		</div>
 		<div class="mdl-card__actions mdl-card--border">
 			<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" data-action="use">
-				<!-- IF themes.skin -->Select Skin<!-- ELSE -->Select Theme<!-- ENDIF themes.skin -->
+				<!-- IF themes.skin -->[[admin/appearance/skins:select-skin]]<!-- ELSE -->[[admin/appearance/themes:select-theme]]<!-- ENDIF themes.skin -->
 			</a>
 		</div>
 	</div>
diff --git a/test/search-admin.js b/test/search-admin.js
new file mode 100644
index 0000000000..216d26d35f
--- /dev/null
+++ b/test/search-admin.js
@@ -0,0 +1,82 @@
+'use strict';
+/*global require*/
+
+var assert = require('assert');
+var search = require('../src/admin/search');
+
+describe('admin search', function () {
+    describe('filterDirectories', function () {
+        it('should resolve all paths to relative paths', function (done) {
+            assert.deepEqual(search.filterDirectories([
+                'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
+            ]), [
+                'admin/gdhgfsdg/sggag',
+            ]);
+            done();
+        });
+        it('should exclude partials', function (done) {
+            assert.deepEqual(search.filterDirectories([
+                'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
+                'dfahdfsgf/admin/partials/hgkfds/fdhsdfh.tpl',
+            ]), [
+                'admin/gdhgfsdg/sggag',
+            ]);
+            done();
+        });
+        it('should exclude files in the admin directory', function (done) {
+            assert.deepEqual(search.filterDirectories([
+                'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
+                'dfdasg/admin/hjkdfsk.tpl',
+            ]), [
+                'admin/gdhgfsdg/sggag',
+            ]);
+            done();
+        });
+    });
+
+    describe('sanitize', function () {
+        it('should strip out scripts', function (done) {
+            assert.equal(
+                search.sanitize('Pellentesque tristique senectus' +
+                    '<script>alert("nope");</script> habitant morbi'),
+                'Pellentesque tristique senectus' +
+                ' habitant morbi'
+            );
+            done();
+        });
+        it('should remove all tags', function (done) {
+            assert.equal(
+                search.sanitize('<p>Pellentesque <b>habitant morbi</b> tristique senectus' +
+                    'Aenean <i>vitae</i> est.Mauris <a href="placerat">eleifend</a> leo.</p>'),
+                'Pellentesque habitant morbi tristique senectus' +
+                'Aenean vitae est.Mauris eleifend leo.'
+            );
+            done();
+        });
+    });
+
+    describe('simplify', function () {
+        it('should remove all mustaches', function (done) {
+            assert.equal(
+                search.simplify(
+                    'Pellentesque tristique {{senectus}}habitant morbi' +
+                    'liquam tincidunt {mauris.eu}risus'
+                ),
+                'Pellentesque tristique habitant morbi' +
+                'liquam tincidunt risus'
+            );
+            done();
+        });
+        it('should collapse all whitespace', function (done) {
+            assert.equal(
+                search.simplify(
+                    'Pellentesque tristique   habitant morbi' +
+                    '  \n\n    liquam tincidunt mauris eu risus.'
+                ),
+                'Pellentesque tristique habitant morbi' +
+                '\nliquam tincidunt mauris eu risus.'
+            );
+            done();
+        });
+    });
+});
\ No newline at end of file
diff --git a/test/translator.js b/test/translator.js
index 9df035cfde..91fbf8f696 100644
--- a/test/translator.js
+++ b/test/translator.js
@@ -233,3 +233,15 @@ describe('Translator modules', function () {
 		done();
 	});
 });
+
+describe('Translator static methods', function () {
+	describe('.removePatterns', function () {
+		it('should remove translator patterns from text', function (done) {
+			assert.strictEqual(
+				Translator.removePatterns('Lorem ipsum dolor [[sit:amet]], consectetur adipiscing elit. [[sed:vitae, [[semper:dolor]]]] lorem'),
+				'Lorem ipsum dolor , consectetur adipiscing elit.  lorem'
+			);
+			done();
+		});
+	});
+});