From 3e60451ce4dc85df00feee31f2c7fdcf0d0a690f Mon Sep 17 00:00:00 2001
From: barisusakli <barisusakli@gmail.com>
Date: Mon, 14 Apr 2014 15:58:13 -0400
Subject: [PATCH] closes #1013

lets user change languages,
---
 public/language/en_GB/global.json   |  3 +-
 public/src/ajaxify.js               |  2 +-
 public/src/forum/accountsettings.js | 26 +++++---
 public/src/translator.js            | 94 +++++++++++++++++------------
 src/controllers/accounts.js         | 22 ++++---
 src/controllers/api.js              |  1 +
 src/meta.js                         | 17 ++----
 src/middleware/middleware.js        | 94 +++++++++++++----------------
 src/socket.io/meta.js               | 11 +++-
 src/user/settings.js                |  4 +-
 10 files changed, 156 insertions(+), 118 deletions(-)

diff --git a/public/language/en_GB/global.json b/public/language/en_GB/global.json
index a1f23f3193..c935f1c5d8 100644
--- a/public/language/en_GB/global.json
+++ b/public/language/en_GB/global.json
@@ -69,5 +69,6 @@
 	"invisible": "Invisible",
 	"offline": "Offline",
 
-	"privacy": "Privacy"
+	"privacy": "Privacy",
+	"language": "Language"
 }
diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js
index d190dbe8fb..885e1bcb6c 100644
--- a/public/src/ajaxify.js
+++ b/public/src/ajaxify.js
@@ -178,7 +178,7 @@ var ajaxify = ajaxify || {};
 				}
 
 				data.relative_path = RELATIVE_PATH;
-				
+
 				templates.parse(tpl_url, data, function(template) {
 					translator.translate(template, function(translatedTemplate) {
 						$('#content').html(translatedTemplate);
diff --git a/public/src/forum/accountsettings.js b/public/src/forum/accountsettings.js
index fcb4c52272..bb18cacb2c 100644
--- a/public/src/forum/accountsettings.js
+++ b/public/src/forum/accountsettings.js
@@ -7,16 +7,21 @@ define(['forum/accountheader'], function(header) {
 		$('#submitBtn').on('click', function() {
 			var settings = {};
 
-			$('.account input, .account textarea').each(function(id, input) {
+			$('.account').find('input, textarea, select').each(function(id, input) {
 				input = $(input);
+				var setting = input.attr('data-property');
+				if (input.is('select')) {
+					settings[setting] = input.val();
+					return;
+				}
 
 				switch (input.attr('type')) {
 					case 'text' :
 					case 'textarea' :
-						settings[input.attr('data-property')] = input.val();
+						settings[setting] = input.val();
 						break;
 					case 'checkbox' :
-						settings[input.attr('data-property')] = input.is(':checked') ? 1 : 0;
+						settings[setting] = input.is(':checked') ? 1 : 0;
 						break;
 				}
 			});
@@ -32,9 +37,16 @@ define(['forum/accountheader'], function(header) {
 		});
 
 		socket.emit('user.getSettings', function(err, settings) {
-			for (var setting in settings) {
-				if (settings.hasOwnProperty(setting)) {
-					var input = $('.account input[data-property="' + setting + '"]');
+			var inputs = $('.account').find('input, textarea, select');
+
+			inputs.each(function(index, input) {
+				input = $(input);
+				var setting = input.attr('data-property');
+				if (setting) {
+					if (input.is('select')) {
+						input.val(settings[setting]);
+						return;
+					}
 
 					switch (input.attr('type')) {
 						case 'text' :
@@ -46,7 +58,7 @@ define(['forum/accountheader'], function(header) {
 							break;
 					}
 				}
-			}
+			});
 		});
 	};
 
diff --git a/public/src/translator.js b/public/src/translator.js
index f795131f38..edfdf6730e 100644
--- a/public/src/translator.js
+++ b/public/src/translator.js
@@ -4,17 +4,15 @@
 
 
 	var translator = {},
-		files = {
-			loaded: {},
-			loading: {},
-			callbacks: {} // could be combined with "loading" in future.
-		};
+		languages = {};
 
 	module.exports = translator;
 
 	// Use this in plugins to add your own translation files.
-	translator.addTranslation = function(filename, translations) {
-		files.loaded[filename] = translations;
+	translator.addTranslation = function(language, filename, translations) {
+		languages[language] = languages[language] || {};
+		languages[language].loaded = languages[language].loaded || {};
+		languages[language].loaded[filename] = translations;
 	};
 
 	translator.getLanguage = function() {
@@ -67,11 +65,21 @@
 		}
 	};
 
-	translator.translate = function (data, callback) {
+	translator.translate = function (data, language, callback) {
 		if (!data) {
 			return callback(data);
 		}
 
+		if (typeof language === 'function') {
+			callback = language;
+			if ('undefined' !== typeof window && config) {
+				language = config.defaultLang || 'en_GB';
+			} else {
+				var meta = require('../../src/meta');
+				language = meta.config.defaultLang || 'en_GB';
+			}
+		}
+
 		function insertLanguage(text, key, value, variables) {
 			if (value) {
 				for (var i = 1, ii = variables.length; i < ii; i++) {
@@ -109,12 +117,12 @@
 			var languageFile = parsedKey[0];
 			parsedKey = ('' + parsedKey[1]).split(',')[0];
 
-			if (files.loaded[languageFile]) {
-				data = insertLanguage(data, key, files.loaded[languageFile][parsedKey], variables);
+			if (isLanguageFileLoaded(language, languageFile)) {
+				data = insertLanguage(data, key, languages[language].loaded[languageFile][parsedKey], variables);
 			} else {
 				loading++;
 				(function (languageKey, parsedKey, languageFile, variables) {
-					translator.load(languageFile, function (languageData) {
+					translator.load(language, languageFile, function (languageData) {
 						data = insertLanguage(data, languageKey, languageData[parsedKey], variables);
 						loading--;
 						checkComplete();
@@ -132,61 +140,73 @@
 		}
 	};
 
-	translator.clearLoadedFiles = function() {
-		files.loaded = {};
-		files.loading = {};
-	};
-
-	translator.load = function (filename, callback) {
+	translator.load = function (language, filename, callback) {
 
-		if (files.loaded[filename] && !files.loading[filename]) {
+		if (isLanguageFileLoaded(language, filename)) {
 			if (callback) {
-				callback(files.loaded[filename]);
+				callback(languages[language].loaded[filename]);
 			}
-		} else if (files.loading[filename]) {
+		} else if (isLanguageFileLoading(language, filename)) {
 			if (callback) {
-				files.callbacks[filename] = files.callbacks[filename] || [];
-				files.callbacks[filename].push(callback);
+				addLanguageFileCallback(language, filename, callback);
 			}
 		} else {
 
-			files.loading[filename] = true;
+			languages[language] = languages[language] || {loading: {}, loaded: {}, callbacks: []};
+
+			languages[language].loading[filename] = true;
+
+			load(language, filename, function(translations) {
 
-			load(filename, function(language) {
-				files.loaded[filename] = language;
+				languages[language].loaded[filename] = translations;
 
 				if (callback) {
-					callback(language);
+					callback(translations);
 				}
 
-				while (files.callbacks[filename] && files.callbacks[filename].length) {
-					files.callbacks[filename].pop()(language);
+				while (languages[language].callbacks[filename] && languages[language].callbacks[filename].length) {
+					languages[language].callbacks[filename].pop()(translations);
 				}
 
-				files.loading[filename] = false;
+				languages[language].loading[filename] = false;
 			});
 		}
 	};
 
-	function load(filename, callback) {
+	function isLanguageFileLoaded(language, filename) {
+		var languageObj = languages[language];
+		return languageObj && languageObj.loaded && languageObj.loaded[filename] && !languageObj.loading[filename];
+	}
+
+	function isLanguageFileLoading(language, filename) {
+		return languages[language] && languages[language].loading && languages[language].loading[filename];
+	}
+
+	function addLanguageFileCallback(language, filename, callback) {
+		languages[language].callbacks = languages[language].callbacks || {};
+
+		languages[language].callbacks[filename] = languages[language].callbacks[filename] || [];
+		languages[language].callbacks[filename].push(callback);
+	}
+
+	function load(language, filename, callback) {
 		if ('undefined' !== typeof window) {
-			loadClient(filename, callback);
+			loadClient(language, filename, callback);
 		} else {
-			loadServer(filename, callback);
+			loadServer(language, filename, callback);
 		}
 	}
 
-	function loadClient(filename, callback) {
+	function loadClient(language, filename, callback) {
 		var timestamp = new Date().getTime();
-		$.getJSON(config.relative_path + '/language/' + config.defaultLang + '/' + filename + '.json?v=' + timestamp, callback);
+		$.getJSON(config.relative_path + '/language/' + language + '/' + filename + '.json?v=' + timestamp, callback);
 	}
 
-	function loadServer(filename, callback) {
+	function loadServer(language, filename, callback) {
 		var fs = require('fs'),
 			path = require('path'),
 			winston = require('winston'),
-			meta = require('../../src/meta'),
-			language = meta.config.defaultLang || 'en_GB';
+			meta = require('../../src/meta');
 
 		if (!fs.existsSync(path.join(__dirname, '../language', language))) {
 			winston.warn('[translator] Language \'' + meta.config.defaultLang + '\' not found. Defaulting to \'en_GB\'');
diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js
index 518c170445..3be58c7b62 100644
--- a/src/controllers/accounts.js
+++ b/src/controllers/accounts.js
@@ -15,6 +15,7 @@ var fs = require('fs'),
 	utils = require('./../../public/src/utils'),
 	meta = require('./../meta'),
 	plugins = require('./../plugins'),
+	languages = require('./../languages'),
 	image = require('./../image'),
 	file = require('./../file');
 
@@ -331,22 +332,29 @@ accountsController.accountSettings = function(req, res, next) {
 				return next(err);
 			}
 
-			user.getUserFields(uid, ['username', 'userslug'], function(err, userData) {
+			async.parallel({
+				user: function(next) {
+					user.getUserFields(uid, ['username', 'userslug'], next);
+				},
+				languages: function(next) {
+					languages.list(next);
+				}
+			}, function(err, results) {
 				if (err) {
 					return next(err);
 				}
 
-				if(!userData) {
+				if(!results.user) {
 					return userNotFound();
 				}
-				userData.yourid = req.user.uid;
-				userData.theirid = uid;
-				userData.settings = settings;
 
-				res.render('accountsettings', userData);
+				results.user.yourid = req.user.uid;
+				results.user.theirid = uid;
+				results.user.settings = settings;
+
+				res.render('accountsettings', results);
 			});
 		});
-
 	});
 };
 
diff --git a/src/controllers/api.js b/src/controllers/api.js
index d136bb5e84..6f1f7042a6 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -58,6 +58,7 @@ apiController.getConfig = function(req, res, next) {
 		config.topicsPerPage = settings.topicsPerPage;
 		config.postsPerPage = settings.postsPerPage;
 		config.notificationSounds = settings.notificationSounds;
+		config.defaultLang = settings.language || config.defaultLang;
 
 		if (res.locals.isAPI) {
 			res.json(200, config);
diff --git a/src/meta.js b/src/meta.js
index 63dfb6629e..c27e318664 100644
--- a/src/meta.js
+++ b/src/meta.js
@@ -63,11 +63,6 @@ var fs = require('fs'),
 
 					callback(err, res);
 				}
-
-				// this might be a good spot to add a hook
-				if (field === 'defaultLang') {
-					translator.clearLoadedFiles();
-				}
 			});
 		},
 		setOnEmpty: function (field, value, callback) {
@@ -173,10 +168,8 @@ var fs = require('fs'),
 			isTopic: /^topic\/\d+\/?/,
 			isUserPage: /^user\/[^\/]+(\/[\w]+)?/
 		},
-		build: function (urlFragment, callback) {
-			var user = require('./user');
-
-			Meta.title.parseFragment(decodeURIComponent(urlFragment), function(err, title) {
+		build: function (urlFragment, language, callback) {
+			Meta.title.parseFragment(decodeURIComponent(urlFragment), language, function(err, title) {
 				if (err) {
 					title = Meta.config.browserTitle || 'NodeBB';
 				} else {
@@ -186,14 +179,14 @@ var fs = require('fs'),
 				callback(null, title);
 			});
 		},
-		parseFragment: function (urlFragment, callback) {
+		parseFragment: function (urlFragment, language, callback) {
 			var	translated = ['', 'recent', 'unread', 'users', 'notifications'];
 			if (translated.indexOf(urlFragment) !== -1) {
 				if (!urlFragment.length) {
 					urlFragment = 'home';
 				}
 
-				translator.translate('[[pages:' + urlFragment + ']]', function(translated) {
+				translator.translate('[[pages:' + urlFragment + ']]', language, function(translated) {
 					callback(null, translated);
 				});
 			} else if (this.tests.isCategory.test(urlFragment)) {
@@ -215,7 +208,7 @@ var fs = require('fs'),
 
 				User.getUsernameByUserslug(userslug, function(err, username) {
 					if (subpage) {
-						translator.translate('[[pages:user.' + subpage + ', ' + username + ']]', function(translated) {
+						translator.translate('[[pages:user.' + subpage + ', ' + username + ']]', language, function(translated) {
 							callback(null, translated);
 						});
 					} else {
diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js
index f56cc9404d..948806572e 100644
--- a/src/middleware/middleware.js
+++ b/src/middleware/middleware.js
@@ -143,30 +143,31 @@ middleware.checkAccountPermissions = function(req, res, next) {
 };
 
 middleware.buildHeader = function(req, res, next) {
-	async.parallel([
-		function(next) {
-			res.locals.renderHeader = true;
-			next();
-		},
-		function(next) {
-			controllers.api.getConfig(req, res, function(err, config) {
-				res.locals.config = config;
-				next(err);
-			});
+	res.locals.renderHeader = true;
+	async.parallel({
+		config: function(next) {
+			controllers.api.getConfig(req, res, next);
 		},
-		function(next) {
-			// consider caching this, since no user specific information is loaded here
-			app.render('footer', {}, function(err, template) {
-				translator.translate(template, function(parsedTemplate) {
-					res.locals.footer = parsedTemplate;
-					next(err);
-				});
-			});
+		footer: function(next) {
+			app.render('footer', {}, next);
+		}
+	}, function(err, results) {
+		if (err) {
+			return next(err);
 		}
-	], next);
+
+		res.locals.config = results.config;
+
+		translator.translate(results.footer, results.config.defaultLang, function(parsedTemplate) {
+			res.locals.footer = parsedTemplate;
+			next();
+		});
+	});
 };
 
 middleware.renderHeader = function(req, res, callback) {
+	var uid = req.user ? parseInt(req.user.uid, 10) : 0;
+
 	var custom_header = {
 		'navigation': []
 	};
@@ -218,8 +219,6 @@ middleware.renderHeader = function(req, res, callback) {
 			}
 		}
 
-		var uid = '0';
-
 		templateValues.metaTags = defaultMetaTags.concat(res.locals.metaTags || []).map(function(tag) {
 			if(!tag || typeof tag.content !== 'string') {
 				winston.warn('Invalid meta tag. ', tag);
@@ -239,9 +238,6 @@ middleware.renderHeader = function(req, res, callback) {
 			href: nconf.get('relative_path') + '/favicon.ico'
 		});
 
-		if(req.user && req.user.uid) {
-			uid = req.user.uid;
-		}
 
 		templateValues.useCustomCSS = false;
 		if (meta.config.useCustomCSS === '1') {
@@ -249,34 +245,30 @@ middleware.renderHeader = function(req, res, callback) {
 			templateValues.customCSS = meta.config.customCSS;
 		}
 
-		async.parallel([
-			function(next) {
-				translator.translate('[[pages:' + path.basename(req.url) + ']]', function(translated) {
-					var	metaTitle = templateValues.metaTags.filter(function(tag) {
-							return tag.name === 'title';
-						});
-
-					if (translated) {
-						templateValues.browserTitle = translated;
-					} else if (metaTitle.length > 0 && metaTitle[0].content) {
-						templateValues.browserTitle = metaTitle[0].content;
-					} else {
-						templateValues.browserTitle = meta.config.browserTitle || 'NodeBB';
-					}
-
-					next();
-				});
+		async.parallel({
+			title: function(next) {
+				if (uid) {
+					user.getSettings(uid, function(err, settings) {
+						if (err) {
+							return next(err);
+						}
+						meta.title.build(req.url.slice(1), settings.language, next);
+					});
+				} else {
+					meta.title.build(req.url.slice(1), meta.config.defaultLang, next);
+				}
 			},
-			function(next) {
-				user.isAdministrator(uid, function(err, isAdmin) {
-					templateValues.isAdmin = isAdmin || false;
-					next();
-				});
+			isAdmin: function(next) {
+				user.isAdministrator(uid, next);
 			}
-		], function() {
-			app.render('header', templateValues, function(err, template) {
-				callback(null, template);
-			});
+		}, function(err, results) {
+			if (err) {
+				return next(err);
+			}
+			templateValues.browserTitle = results.title;
+			templateValues.isAdmin = results.isAdmin || false;
+
+			app.render('header', templateValues, callback);
 		});
 	});
 };
@@ -322,7 +314,7 @@ middleware.processRender = function(req, res, next) {
 				middleware.renderHeader(req, res, function(err, template) {
 					str = template + str;
 
-					translator.translate(str, function(translated) {
+					translator.translate(str, res.locals.config.defaultLang, function(translated) {
 						fn(err, translated);
 					});
 				});
diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js
index e2733e33a7..e583b9ad21 100644
--- a/src/socket.io/meta.js
+++ b/src/socket.io/meta.js
@@ -32,7 +32,16 @@ SocketMeta.reconnected = function(socket) {
 };
 
 SocketMeta.buildTitle = function(socket, text, callback) {
-	meta.title.build(text, callback);
+	if (socket.uid) {
+		user.getSettings(socket.uid, function(err, settings) {
+			if (err) {
+				return callback(err);
+			}
+			meta.title.build(text, settings.language, callback);
+		});
+	} else {
+		meta.title.build(text, meta.config.defaultLang, callback);
+	}
 };
 
 SocketMeta.updateHeader = function(socket, data, callback) {
diff --git a/src/user/settings.js b/src/user/settings.js
index 7e9665d9f0..35ca90717d 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -29,6 +29,7 @@ module.exports = function(User) {
 				settings.topicsPerPage = settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : parseInt(meta.config.topicsPerPage, 10) || 20;
 				settings.postsPerPage = settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : parseInt(meta.config.postsPerPage, 10) || 10;
 				settings.notificationSounds = settings.notificationSounds ? parseInt(settings.notificationSounds, 10) === 1 : true;
+				settings.language = settings.language || meta.config.defaultLang || 'en_GB';
 				callback(null, settings);
 			});
 		});
@@ -47,7 +48,8 @@ module.exports = function(User) {
 			usePagination: data.usePagination,
 			topicsPerPage: data.topicsPerPage,
 			postsPerPage: data.postsPerPage,
-			notificationSounds: data.notificationSounds
+			notificationSounds: data.notificationSounds,
+			language: data.language || meta.config.defaultLang
 		}, callback);
 	};
 };
\ No newline at end of file