Rewrite translator for efficiency and clarity (#5013)
* Rewrite translator for efficiency and clarity * Fixed failing tests * Gorram tabs * Move cache to Translator class, simplify creation * Documentation and linting * Use new translator API where it makes sense * Revert 499fbe21fdd5b7f2735a27e76a300f69039cfbd3 (except translator.d.ts) * promise polyfill for node<4 * Set `load` on the prototype * Comment on Translator#translate * Delete translator.d.ts * Support translations added by pluginsv1.18.x
parent
ef6e72c246
commit
59ed13b581
@ -1,347 +1,416 @@
|
||||
;(function(translator) {
|
||||
"use strict";
|
||||
/* globals RELATIVE_PATH, config, define */
|
||||
/* global define, jQuery, config, RELATIVE_PATH, utils, window, Promise, winston */
|
||||
|
||||
var S = null;
|
||||
var stringDefer = null;
|
||||
(function (factory) {
|
||||
'use strict';
|
||||
function loadClient(language, filename) {
|
||||
return Promise.resolve(jQuery.getJSON(config.relative_path + '/language/' + language + '/' + (filename + '.json?v=' + config['cache-buster'])));
|
||||
}
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as a named module
|
||||
define('translator', ['string'], function (string) {
|
||||
return factory(string, loadClient);
|
||||
});
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// Node
|
||||
(function () {
|
||||
require('promise-polyfill');
|
||||
var languages = require('../../../src/languages');
|
||||
|
||||
function loadServer(language, filename) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
languages.get(language, filename + '.json', function (err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// export the class if we are in a Node-like system.
|
||||
if (typeof module === 'object' && module.exports === translator) {
|
||||
exports = module.exports = translator;
|
||||
S = require('string');
|
||||
module.exports = factory(require('string'), loadServer);
|
||||
})();
|
||||
} else {
|
||||
stringDefer = $.Deferred();
|
||||
require(['string'], function(stringLib) {
|
||||
S = stringLib;
|
||||
stringDefer.resolve(S);
|
||||
});
|
||||
window.translator = factory(window.string, loadClient);
|
||||
}
|
||||
})(function (string, load) {
|
||||
'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"); } }
|
||||
|
||||
var Translator = function () {
|
||||
/**
|
||||
* Construct a new Translator object
|
||||
* @param {string} language - Language code for this translator instance
|
||||
*/
|
||||
function Translator(language) {
|
||||
classCallCheck(this, Translator);
|
||||
|
||||
if (!language) {
|
||||
throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : ''));
|
||||
}
|
||||
|
||||
var languages = {},
|
||||
regexes = {
|
||||
match: /\[\[\w+:[\w\.]+((?!\[\[).)*?\]\]/g, // see tests/translator.js for an explanation re: this monster
|
||||
split: /[,][\s]*/,
|
||||
replace: /\]+$/
|
||||
};
|
||||
|
||||
translator.addTranslation = function(language, filename, translations) {
|
||||
languages[language] = languages[language] || {};
|
||||
languages[language].loaded = languages[language].loaded || {};
|
||||
languages[language].loading = languages[language].loading || {};
|
||||
this.lang = language;
|
||||
this.translations = {};
|
||||
}
|
||||
|
||||
if (languages[language].loaded[filename]) {
|
||||
for (var t in translations) {
|
||||
if (translations.hasOwnProperty(t)) {
|
||||
languages[language].loaded[filename][t] = translations[t];
|
||||
Translator.prototype.load = load;
|
||||
|
||||
/**
|
||||
* Parse the translation instructions into the language of the Translator instance
|
||||
* @param {string} str - Source string
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
Translator.prototype.translate = function translate(str) {
|
||||
// current cursor position
|
||||
var cursor = 0;
|
||||
// last break of the input string
|
||||
var lastBreak = 0;
|
||||
// length of the input string
|
||||
var len = str.length;
|
||||
// array to hold the promises for the translations
|
||||
// and the strings of untranslated text in between
|
||||
var toTranslate = [];
|
||||
|
||||
// split a translator string into an array of tokens
|
||||
// but don't split by commas inside other translator strings
|
||||
function split(text) {
|
||||
var len = text.length;
|
||||
var arr = [];
|
||||
var i = 0;
|
||||
var brk = 0;
|
||||
var level = 0;
|
||||
|
||||
while (i + 2 <= len) {
|
||||
if (text.slice(i, i + 2) === '[[') {
|
||||
level += 1;
|
||||
i += 1;
|
||||
} else if (text.slice(i, i + 2) === ']]') {
|
||||
level -= 1;
|
||||
i += 1;
|
||||
} else if (level === 0 && text[i] === ',') {
|
||||
arr.push(text.slice(brk, i).trim());
|
||||
i += 1;
|
||||
brk = i;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
arr.push(text.slice(brk, i + 1).trim());
|
||||
return arr;
|
||||
}
|
||||
} else {
|
||||
languages[language].loaded[filename] = translations;
|
||||
}
|
||||
};
|
||||
|
||||
translator.getTranslations = function(language, filename, callback) {
|
||||
if (languages[language] && languages[language].loaded[filename]) {
|
||||
callback(languages[language].loaded[filename]);
|
||||
} else {
|
||||
translator.load(language, filename, function() {
|
||||
callback(languages[language].loaded[filename]);
|
||||
});
|
||||
}
|
||||
};
|
||||
// the loooop, we'll go to where the cursor
|
||||
// is equal to the length of the string since
|
||||
// slice doesn't include the ending index
|
||||
while (cursor + 2 <= len) {
|
||||
// if the current position in the string looks
|
||||
// like the beginning of a translation string
|
||||
if (str.slice(cursor, cursor + 2) === '[[') {
|
||||
// split the string from the last break
|
||||
// to the character before the cursor
|
||||
// add that to the result array
|
||||
toTranslate.push(str.slice(lastBreak, cursor));
|
||||
// set the cursor position past the beginning
|
||||
// brackets of the translation string
|
||||
cursor += 2;
|
||||
// set the last break to our current
|
||||
// spot since we just broke the string
|
||||
lastBreak = cursor;
|
||||
|
||||
// the current level of nesting of the translation strings
|
||||
var level = 0;
|
||||
var sliced;
|
||||
|
||||
while (cursor + 2 <= len) {
|
||||
sliced = str.slice(cursor, cursor + 2);
|
||||
// if we're at the beginning of another translation string,
|
||||
// we're nested, so add to our level
|
||||
if (sliced === '[[') {
|
||||
level += 1;
|
||||
cursor += 2;
|
||||
// if we're at the end of a translation string
|
||||
} else if (sliced === ']]') {
|
||||
// if we're at the base level, then this is the end
|
||||
if (level === 0) {
|
||||
// so grab the name and args
|
||||
var result = split(str.slice(lastBreak, cursor));
|
||||
var name = result[0];
|
||||
var args = result.slice(1);
|
||||
|
||||
// add the translation promise to the array
|
||||
toTranslate.push(this.translateKey(name, args));
|
||||
// skip past the ending brackets
|
||||
cursor += 2;
|
||||
// set this as our last break
|
||||
lastBreak = cursor;
|
||||
// and we're no longer in a translation string,
|
||||
// so continue with the main loop
|
||||
break;
|
||||
}
|
||||
// otherwise we lower the level
|
||||
level -= 1;
|
||||
// and skip past the ending brackets
|
||||
cursor += 2;
|
||||
} else {
|
||||
// otherwise just move to the next character
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// move to the next character
|
||||
cursor += 1;
|
||||
}
|
||||
|
||||
translator.escape = function(text) {
|
||||
return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text;
|
||||
};
|
||||
// add the remaining text after the last translation string
|
||||
toTranslate.push(str.slice(lastBreak, cursor + 2));
|
||||
|
||||
translator.unescape = function(text) {
|
||||
return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text;
|
||||
};
|
||||
// and return a promise for the concatenated translated string
|
||||
return Promise.all(toTranslate).then(function (translated) {
|
||||
return translated.join('');
|
||||
});
|
||||
};
|
||||
|
||||
translator.getLanguage = function() {
|
||||
return config.defaultLang;
|
||||
};
|
||||
/**
|
||||
* Translates a specific key and array of arguments
|
||||
* @param {string} name - Translation key (ex. 'global:home')
|
||||
* @param {string[]} args - Arguments for `%1`, `%2`, etc
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
Translator.prototype.translateKey = function translateKey(name, args) {
|
||||
var self = this;
|
||||
|
||||
var result = name.split(':', 2);
|
||||
var namespace = result[0];
|
||||
var key = result[1];
|
||||
|
||||
var translation = this.getTranslation(namespace, key);
|
||||
var argsToTranslate = args.map(function (arg) {
|
||||
return string(arg).collapseWhitespace().decodeHTMLEntities().escapeHTML().s;
|
||||
}).map(function (arg) {
|
||||
return self.translate(arg);
|
||||
});
|
||||
|
||||
translator.prepareDOM = function() {
|
||||
// Load the appropriate timeago locale file, and correct NodeBB language codes to timeago codes, if necessary
|
||||
var languageCode;
|
||||
switch(config.userLang) {
|
||||
case 'en_GB':
|
||||
case 'en_US':
|
||||
languageCode = 'en';
|
||||
break;
|
||||
|
||||
case 'fa_IR':
|
||||
languageCode = 'fa';
|
||||
break;
|
||||
|
||||
case 'pt_BR':
|
||||
languageCode = 'pt-br';
|
||||
break;
|
||||
|
||||
case 'nb':
|
||||
languageCode = 'no';
|
||||
break;
|
||||
|
||||
case 'zh_TW':
|
||||
languageCode = 'zh-TW';
|
||||
break;
|
||||
|
||||
case 'zh_CN':
|
||||
languageCode = 'zh-CN';
|
||||
break;
|
||||
|
||||
default:
|
||||
languageCode = config.userLang;
|
||||
break;
|
||||
}
|
||||
// so we can await all promises at once
|
||||
argsToTranslate.unshift(translation);
|
||||
|
||||
$.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function() {
|
||||
$('.timeago').timeago();
|
||||
translator.timeagoShort = $.extend({}, jQuery.timeago.settings.strings);
|
||||
return Promise.all(argsToTranslate).then(function (result) {
|
||||
var translated = result[0];
|
||||
var translatedArgs = result.slice(1);
|
||||
|
||||
// Retrieve the shorthand timeago values as well
|
||||
$.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function() {
|
||||
// Switch back to long-form
|
||||
translator.toggleTimeagoShorthand();
|
||||
if (!translated) {
|
||||
return key;
|
||||
}
|
||||
var out = translated;
|
||||
translatedArgs.forEach(function (arg, i) {
|
||||
out = out.replace(new RegExp('%' + (i + 1), 'g'), arg);
|
||||
});
|
||||
return out;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Add directional code if necessary
|
||||
translator.translate('[[language:dir]]', function(value) {
|
||||
if (value) {
|
||||
$('html').css('direction', value).attr('data-dir', value);
|
||||
/**
|
||||
* Load translation file (or use a cached version), and optionally return the translation of a certain key
|
||||
* @param {string} namespace - The file name of the translation namespace
|
||||
* @param {string} [key] - The key of the specific translation to getJSON
|
||||
* @returns {Promise<Object|string>}
|
||||
*/
|
||||
Translator.prototype.getTranslation = function getTranslation(namespace, key) {
|
||||
var translation;
|
||||
if (!namespace) {
|
||||
winston.warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : ''));
|
||||
translation = Promise.resolve({});
|
||||
} else if (this.translations[namespace]) {
|
||||
translation = this.translations[namespace];
|
||||
} else {
|
||||
translation = this.load(this.lang, namespace);
|
||||
this.translations[namespace] = translation;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
translator.toggleTimeagoShorthand = function() {
|
||||
var tmp = $.extend({}, jQuery.timeago.settings.strings);
|
||||
jQuery.timeago.settings.strings = $.extend({}, translator.timeagoShort);
|
||||
translator.timeagoShort = $.extend({}, tmp);
|
||||
};
|
||||
if (key) {
|
||||
return translation.then(function (x) {
|
||||
return x[key];
|
||||
});
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the language of the current environment, falling back to defaults
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.getLanguage = function getLanguage() {
|
||||
var lang;
|
||||
|
||||
translator.translate = function (text, language, callback) {
|
||||
if (typeof language === 'function') {
|
||||
callback = language;
|
||||
if ('undefined' !== typeof window && config) {
|
||||
language = utils.params().lang || config.userLang || 'en_GB';
|
||||
if (typeof window === 'object' && window.config && window.utils) {
|
||||
lang = utils.params().lang || config.userLang || config.defaultLang || 'en_GB';
|
||||
} else {
|
||||
var meta = require('../../../src/meta');
|
||||
language = meta.config.defaultLang || 'en_GB';
|
||||
lang = meta.config.defaultLang || 'en_GB';
|
||||
}
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return callback(text);
|
||||
}
|
||||
|
||||
var keys = text.match(regexes.match);
|
||||
|
||||
if (!keys) {
|
||||
return callback(text);
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
translateKeys(keys, text, language, function(translated) {
|
||||
keys = translated.match(regexes.match);
|
||||
if (!keys) {
|
||||
callback(translated);
|
||||
} else {
|
||||
translateKeys(keys, translated, language, callback);
|
||||
/**
|
||||
* Create and cache a new Translator instance, or return a cached one
|
||||
* @param {string} [language] - ('en_GB') Language string
|
||||
* @returns {Translator}
|
||||
*/
|
||||
Translator.create = function create(language) {
|
||||
if (!language) {
|
||||
language = Translator.getLanguage();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function translateKeys(keys, text, language, callback) {
|
||||
|
||||
var count = keys.length;
|
||||
if (!count) {
|
||||
return callback(text);
|
||||
}
|
||||
Translator.cache[language] = Translator.cache[language] || new Translator(language);
|
||||
|
||||
if (S === null) { // browser environment and S not yet initialized
|
||||
// we need to wait for async require call
|
||||
stringDefer.then(function () { translateKeys(keys, text, language, callback); });
|
||||
return;
|
||||
}
|
||||
return Translator.cache[language];
|
||||
};
|
||||
|
||||
Translator.cache = {};
|
||||
|
||||
return Translator;
|
||||
}();
|
||||
|
||||
var adaptor = {
|
||||
/**
|
||||
* The Translator class
|
||||
*/
|
||||
Translator: Translator,
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
|
||||
var cb = callback;
|
||||
var lang = language;
|
||||
if (typeof language === 'function') {
|
||||
cb = language;
|
||||
lang = null;
|
||||
}
|
||||
|
||||
var data = {text: text};
|
||||
keys.forEach(function(key) {
|
||||
translateKey(key, data, language, function(translated) {
|
||||
--count;
|
||||
if (count <= 0) {
|
||||
callback(translated.text);
|
||||
}
|
||||
Translator.create(lang).translate(text).then(function (output) {
|
||||
return cb(output);
|
||||
}).catch(function (err) {
|
||||
console.error('Translation failed: ' + err.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function translateKey(key, data, language, callback) {
|
||||
key = '' + key;
|
||||
var variables = key.split(regexes.split);
|
||||
|
||||
var parsedKey = key.replace('[[', '').replace(']]', '').split(':');
|
||||
parsedKey = [parsedKey[0]].concat(parsedKey.slice(1).join(':'));
|
||||
if (!(parsedKey[0] && parsedKey[1])) {
|
||||
return callback(data);
|
||||
}
|
||||
|
||||
var languageFile = parsedKey[0];
|
||||
parsedKey = ('' + parsedKey[1]).split(',')[0];
|
||||
|
||||
translator.load(language, languageFile, function(languageData) {
|
||||
data.text = insertLanguage(data.text, key, languageData[parsedKey], variables);
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
|
||||
function insertLanguage(text, key, value, variables) {
|
||||
if (value) {
|
||||
variables.forEach(function(variable, index) {
|
||||
if (index > 0) {
|
||||
variable = S(variable).chompRight(']]').collapseWhitespace().decodeHTMLEntities().escapeHTML().s;
|
||||
value = value.replace('%' + index, function() { return variable; });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
addTranslation: function addTranslation(language, filename, translation) {
|
||||
Translator.create(language).getTranslation(filename).then(function (translations) {
|
||||
assign(translations, translation);
|
||||
});
|
||||
|
||||
text = text.replace(key, function() { return value; });
|
||||
} else {
|
||||
var string = key.split(':');
|
||||
text = text.replace(key, string[string.length-1].replace(regexes.replace, ''));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
translator.compile = function() {
|
||||
var args = Array.prototype.slice.call(arguments, 0);
|
||||
|
||||
return '[[' + args.join(', ') + ']]';
|
||||
};
|
||||
|
||||
translator.load = function (language, filename, callback) {
|
||||
if (isLanguageFileLoaded(language, filename)) {
|
||||
if (callback) {
|
||||
callback(languages[language].loaded[filename]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the translations object
|
||||
*/
|
||||
getTranslations: function getTranslations(language, filename, callback) {
|
||||
callback = callback || function () {};
|
||||
Translator.create(language).getTranslation(filename).then(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Alias of getTranslations
|
||||
*/
|
||||
load: function load(language, filename, callback) {
|
||||
adaptor.getTranslations(language, filename, 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);
|
||||
adaptor.timeagoShort = assign({}, tmp);
|
||||
},
|
||||
prepareDOM: function prepareDOM() {
|
||||
// Load the appropriate timeago locale file,
|
||||
// and correct NodeBB language codes to timeago codes, if necessary
|
||||
var languageCode = void 0;
|
||||
switch (config.userLang) {
|
||||
case 'en_GB':
|
||||
case 'en_US':
|
||||
languageCode = 'en';
|
||||
break;
|
||||
|
||||
case 'fa_IR':
|
||||
languageCode = 'fa';
|
||||
break;
|
||||
|
||||
case 'pt_BR':
|
||||
languageCode = 'pt-br';
|
||||
break;
|
||||
|
||||
case 'nb':
|
||||
languageCode = 'no';
|
||||
break;
|
||||
|
||||
case 'zh_TW':
|
||||
languageCode = 'zh-TW';
|
||||
break;
|
||||
|
||||
case 'zh_CN':
|
||||
languageCode = 'zh-CN';
|
||||
break;
|
||||
|
||||
default:
|
||||
languageCode = config.userLang;
|
||||
break;
|
||||
}
|
||||
} else if (isLanguageFileLoading(language, filename)) {
|
||||
if (callback) {
|
||||
addLanguageFileCallback(language, filename, callback);
|
||||
}
|
||||
} else {
|
||||
|
||||
languages[language] = languages[language] || {loading: {}, loaded: {}, callbacks: []};
|
||||
|
||||
languages[language].loading[filename] = true;
|
||||
|
||||
load(language, filename, function(translations) {
|
||||
|
||||
languages[language].loaded[filename] = translations;
|
||||
jQuery.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () {
|
||||
jQuery('.timeago').timeago();
|
||||
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
|
||||
|
||||
if (callback) {
|
||||
callback(translations);
|
||||
}
|
||||
// Retrieve the shorthand timeago values as well
|
||||
jQuery.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () {
|
||||
// Switch back to long-form
|
||||
adaptor.toggleTimeagoShorthand();
|
||||
});
|
||||
});
|
||||
|
||||
while (languages[language].callbacks && languages[language].callbacks[filename] && languages[language].callbacks[filename].length) {
|
||||
languages[language].callbacks[filename].pop()(translations);
|
||||
// Add directional code if necessary
|
||||
adaptor.translate('[[language:dir]]', function (value) {
|
||||
if (value) {
|
||||
jQuery('html').css('direction', value).attr('data-dir', value);
|
||||
}
|
||||
|
||||
languages[language].loading[filename] = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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(language, filename, callback);
|
||||
} else {
|
||||
loadServer(language, filename, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function loadClient(language, filename, callback) {
|
||||
$.getJSON(config.relative_path + '/language/' + language + '/' + filename + '.json?v=' + config['cache-buster'], callback);
|
||||
}
|
||||
|
||||
function loadServer(language, filename, callback) {
|
||||
var fs = require('fs'),
|
||||
path = require('path'),
|
||||
winston = require('winston'),
|
||||
_ = require('underscore'),
|
||||
file = require('../../../src/file'),
|
||||
plugins = require('../../../src/plugins'),
|
||||
meta = require('../../../src/meta');
|
||||
|
||||
var hash = language + '/' + filename + '.json';
|
||||
|
||||
language = language || meta.config.defaultLang || 'en_GB';
|
||||
|
||||
if (!file.existsSync(path.join(__dirname, '../../language', language))) {
|
||||
winston.warn('[translator] Language \'' + meta.config.defaultLang + '\' not found. Defaulting to \'en_GB\'');
|
||||
language = 'en_GB';
|
||||
}
|
||||
|
||||
fs.readFile(path.join(__dirname, '../../language', hash), function(err, data) {
|
||||
var onData = function(data) {
|
||||
try {
|
||||
data = JSON.parse(data.toString());
|
||||
|
||||
if (plugins.customLanguages.hasOwnProperty(hash)) {
|
||||
_.extendOwn(data, plugins.customLanguages[hash]);
|
||||
}
|
||||
} catch (e) {
|
||||
winston.error('Could not parse `' + filename + '.json`, syntax error? Skipping...');
|
||||
data = {};
|
||||
}
|
||||
|
||||
callback(data);
|
||||
};
|
||||
|
||||
if (err && err.code === 'ENOENT') {
|
||||
data = '{}';
|
||||
} else if (err) {
|
||||
winston.error('[translator] Error while loading language file: ' + err.message);
|
||||
return callback({});
|
||||
}
|
||||
|
||||
onData(data);
|
||||
});
|
||||
}
|
||||
|
||||
// Use the define() function if we're in AMD land
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('translator', translator);
|
||||
|
||||
var _translator = translator;
|
||||
|
||||
// Expose a global `translator` object for backwards compatibility
|
||||
window.translator = {
|
||||
translate: function() {
|
||||
if (typeof console !== 'undefined' && console.warn) {
|
||||
console.warn('[translator] Global invocation of the translator is now deprecated, please `require` the module instead.');
|
||||
}
|
||||
_translator.translate.apply(_translator, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
})(
|
||||
typeof exports === 'object' ? exports :
|
||||
typeof define === 'function' && define.amd ? {} :
|
||||
translator = {}
|
||||
);
|
||||
return adaptor;
|
||||
});
|
||||
|
@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
/*global require*/
|
||||
|
||||
var assert = require('assert');
|
||||
var Translator = require('../public/src/modules/translator.js').Translator;
|
||||
|
||||
|
||||
describe('new Translator(language)', function(){
|
||||
describe('.translate()', function(){
|
||||
it('should handle basic translations', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('[[global:home]]').then(function(translated) {
|
||||
assert.strictEqual(translated, 'Home');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle language keys in regular text', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('Let\'s go [[global:home]]').then(function(translated) {
|
||||
assert.strictEqual(translated, 'Let\'s go Home');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept a language parameter and adjust accordingly', function(done) {
|
||||
var translator = new Translator('de');
|
||||
|
||||
translator.translate('[[global:home]]').then(function(translated) {
|
||||
assert.strictEqual(translated, 'Übersicht');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle language keys in regular text with another language specified', function(done) {
|
||||
var translator = new Translator('de');
|
||||
|
||||
translator.translate('[[global:home]] test').then(function(translated) {
|
||||
assert.strictEqual(translated, 'Übersicht test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle language keys with parameters', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('[[global:pagination.out_of, 1, 5]]').then(function(translated) {
|
||||
assert.strictEqual(translated, '1 out of 5');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle language keys inside language keys', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('[[notifications:outgoing_link_message, [[global:guest]]]]').then(function(translated) {
|
||||
assert.strictEqual(translated, 'You are now leaving Guest');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle language keys inside language keys with multiple parameters', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('[[notifications:user_posted_to, [[global:guest]], My Topic]]').then(function(translated) {
|
||||
assert.strictEqual(translated, '<strong>Guest</strong> has posted a reply to: <strong>My Topic</strong>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle language keys inside language keys with all parameters as language keys', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('[[notifications:user_posted_to, [[global:guest]], [[global:guest]]]]').then(function(translated) {
|
||||
assert.strictEqual(translated, '<strong>Guest</strong> has posted a reply to: <strong>Guest</strong>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly handle parameters that contain square brackets', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('[[global:pagination.out_of, [guest], [[global:home]]]]').then(function(translated) {
|
||||
assert.strictEqual(translated, '[guest] out of Home');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly handle parameters that contain parentheses', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
translator.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]').then(function(translated) {
|
||||
assert.strictEqual(translated, '(foobar) out of Home');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not translate language key parameters with HTML in them', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
var key = '[[global:403.login, <strong>test</strong>]]';
|
||||
translator.translate(key).then(function(translated) {
|
||||
assert.strictEqual(translated, 'Perhaps you should <a href=\'<strong>test</strong>/login\'>try logging in</a>?');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly escape % and ,', function(done) {
|
||||
var translator = new Translator('en_GB');
|
||||
|
||||
var title = 'Test 1, 2, 3 % salmon';
|
||||
title = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
var key = "[[topic:composer.replying_to, " + title + "]]";
|
||||
translator.translate(key).then(function(translated) {
|
||||
assert.strictEqual(translated, 'Replying to Test 1, 2, 3 % salmon');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if not passed a language', function(done) {
|
||||
assert.throws(function () {
|
||||
new Translator();
|
||||
}, /language string/);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('Translator.create()', function(){
|
||||
describe('.translate()', function(){
|
||||
it('should return an instance of Translator', function(done) {
|
||||
var translator = Translator.create('en_GB');
|
||||
|
||||
assert(translator instanceof Translator);
|
||||
done();
|
||||
});
|
||||
it('should return the same object for the same language', function(done) {
|
||||
var one = Translator.create('de');
|
||||
var two = Translator.create('de');
|
||||
|
||||
assert.strictEqual(one, two);
|
||||
done();
|
||||
});
|
||||
it('should default to defaultLang', function(done) {
|
||||
var translator = Translator.create();
|
||||
|
||||
assert.strictEqual(translator.lang, 'en_GB');
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue