(function($) {
'use strict';
class Options {
constructor() {
// Default options
this.lang = null
this.collectMissing = true
this.showMissing = false
this.test = false
this.dynamicDataPlaceholders = true // Whether to detect content that looks like dynamic and replace it with placeholders
this.wordCharRegex = 'a-zà-öø-ÿ0-9'
this.groupNodeNames = ['A', 'IMG'] // Variable containing the default group node names: ['A', 'IMG'].
this.mandatoryTranslateRegex = []
/**
* Element selectors that require attribute translation
* Key value pairs: { selector: [ attributeName1, attributeName2, ... ] }
* Attribute `attributeName*` will be translated for elementes with `selector`
*/
this.translateAttributes = {
'input[type="button"]': [ 'value' ],
'input[type="submit"]': [ 'value' ],
'input[placeholder]': [ 'placeholder' ],
'img': [ 'src', 'alt' ],
'a': [ 'href' ]
}
}
get(propertyName) {
return this[propertyName];
}
only(keys) {
const result = {};
keys.forEach(key => result[key] = this.get(key));
return result;
}
// Regex to match non-word chars (not for translation)
// Include digits to allow sentences such as "1st time" to be translated including number
getWordChRegexStr() {
return '[' + this.wordCharRegex + ']'
}
getNonWordChRegexStr() {
return '[^' + this.wordCharRegex + ']'
}
// Regex to detect word start (word may start with opening parenthesis)
getWordStartRegexStr() {
const wordChRegexStr = this.getWordChRegexStr()
return '(?:' +
'(?:' +
'[\\[({<]+' +
'\\s*' +
')*' +
wordChRegexStr +
')';
}
// Regex to detect word end
getWordEndRegexStr() {
const wordChRegexStr = this.getWordChRegexStr()
return '(?:' +
wordChRegexStr +
'(?:' +
'\\s*' +
'[\\])}>]+' +
')*' +
')';
}
// Regex to ignore numbers, dates, emails, etc.
getIgnoreRegexStr() {
return '(?:' +
'(?:' +
'\\d+' + // Starts with a digit
'(?:' +
'[()\\[\\]:.,\\s=*/+-]+' + // Delimeters used in dates, time, numbers, math expressions
'\\d+' + // Followed by a number
')*' + // May be repeated one or more times
'(?:\\s*(?:am|pm))?' + // Ignore am/pm time
')|(?:' +
'\\S+@\\S+\\.\\S+' + // Email match
')' +
')';
}
// Ignored if there is full sentence match of ignored chars
getIgnoreRegexFullStr() {
const nonWordChRegexStr = this.getNonWordChRegexStr()
return '(?:' +
nonWordChRegexStr + '+' +
')';
}
// Regex to detect parts that should not be translated
getIgnoreRegex() {
const ignoreRegexStr = this.getIgnoreRegexStr()
return new RegExp('\\b(' + ignoreRegexStr + ')\\b', 'gi')
}
getIgnoreRegexFull() {
const ignoreRegexStr = this.getIgnoreRegexStr()
const ignoreRegexFullStr = this.getIgnoreRegexFullStr()
return new RegExp('^(?:' + ignoreRegexStr + '|' + ignoreRegexFullStr + ')$', 'i');
}
// Regex used in trim()
getTrimStartRegex() {
const nonWordChRegexStr = this.getNonWordChRegexStr()
const wordStartRegexStr = this.getWordStartRegexStr()
return new RegExp('^(' + nonWordChRegexStr + '*?)(' + wordStartRegexStr + ')', 'i');
}
getTrimEndRegex() {
const wordEndRegexStr = this.getWordEndRegexStr()
const nonWordChRegexStr = this.getNonWordChRegexStr()
return new RegExp('(' + wordEndRegexStr + ')(' + nonWordChRegexStr + '*?)$', 'i');
}
instance(props) {
Object.assign(this, props);
return this
}
}
/**
* TranslationNode holds jQuery element to be translated. In case all child elements
* are inline elements, the whole html will be sent to translation as a single piece.
* If at least 1 child element is a block-level element, child nodes will be translated
* one-by-one.
* @param {jQuery} elem jQuery element to be translated
* @param {TranslationNode} parentNode parent TranslationNode in DOM
*/
function TranslationNode(elem, parentNode) {
if (!(this instanceof TranslationNode)) {
throw new Error('TranslationNode must be created with the new keyword');
}
if (elem.length != 1) {
throw new Error('TranslationNode() - only single elements should be added for translation');
}
this._elem = elem;
this._parentNode = parentNode || null;
// See isSinglePiece()
this._isSinglePiece = this.isTextNode() || !this.isBreakTranslate();
this._childTranslationNodes = [];
}
TranslationNode.prototype.getUuid = function () {
if (this._uuid === undefined) {
this._uuid = this.generateUuid();
}
return this._uuid
}
TranslationNode.prototype.generateUuid = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
TranslationNode.prototype.getAllParentNodes = function () {
if (this._parentNodes === undefined) {
this._parentNodes = [];
if (this._parentNode) {
this._parentNodes = this._parentNodes.concat(this._parentNode, this._parentNode.getAllParentNodes());
}
}
return this._parentNodes;
}
/**
* Recursievely translate current node
*/
TranslationNode.prototype.translate = function(options) {
var singlePieceNodes = this.getSinglePieceNodes();
for (var i = 0; i < singlePieceNodes.length; i++) {
var translationSentence = new TranslationSentence(singlePieceNodes[i], options);
translationSentence.translate();
}
};
/**
* Recursievely translate current node
*/
TranslationNode.prototype.getTranslationTexts = function(options) {
return this.getSinglePieceNodes().map(function(singlePieceNode) {
var translationSentence = new TranslationSentence(singlePieceNode, options);
return translationSentence.getText();
});
};
/**
* Get single-piece translatable nodes only
*
* @return {array} of single-piece TranslationNodes
*/
TranslationNode.prototype.getSinglePieceNodes = function() {
if (this.isSinglePiece() && !this.isNoTranslate()) {
return [ this ];
}
return this._childTranslationNodes.reduce(function(nodes, translationNode) {
return nodes.concat(translationNode.getSinglePieceNodes());
}, []);
}
TranslationNode.prototype.getNodeNameGroupNodes = function () {
if (this.isSinglePiece() && !this.isNoTranslate()) {
return [ this ];
}
if (this.isNodeNameGroup()) {
return [ this ]
}
return this._childTranslationNodes.reduce(function(nodes, translationNode) {
return nodes.concat(translationNode.getNodeNameGroupNodes());
}, []);
}
/**
* Get all non-empty child text nodes of current single-piece TranslationNode
*
* @return {array} of TranslationNode text-nodes
*/
TranslationNode.prototype.getTextNodes = function() {
if (this.isIgnored() || !this.isSinglePiece()) {
return [];
}
if (this.isTextNode()) {
return [ this ];
}
if (this.isAttributeNode()) {
return [ this ];
}
return this._childTranslationNodes.reduce(function(nodes, translationNode) {
return nodes.concat(translationNode.getTextNodes());
}, []);
}
TranslationNode.prototype.revert = function(setText) {
if (this._updatedText && setText) {
this.setText(this.getText());
}
this._updatedText = null;
this._updatedTextLen = null;
for (var i = 0; i < this._childTranslationNodes.length; i++) {
this._childTranslationNodes[i].revert(setText);
}
}
/**
* Get node text
*
* @returns {string}
*/
TranslationNode.prototype.getText = function() {
if (this._text === undefined) {
this._text = this.isAttributeNode() ?
this._elem.attr(this._translateAttribute) :
this._elem.text();
}
return this._text;
}
/**
* Change node text. May provide start and end points if only portion of the text
* should be updated. start and end points are relative to original text and sometimes
* we need to update the text multiple times. To correctly replace the text, we keep
* track of text length updates
*
* @param {string} text new text
* @param {integer} start start point (may be undefined if whole text should be replaced)
* @param {integer} end end point (may be undefined if whole text should be replaced)
*/
TranslationNode.prototype.setText = function(text, start, end) {
this._updatedText = this._updatedText || this.getText();
this._updatedTextLen = this._updatedTextLen || [];
var oldText = this._updatedText;
if (start !== undefined && end !== undefined) {
var lenChanged = 0;
for (var i = 0; i < this._updatedTextLen.length; i++) {
var textLenUpdate = this._updatedTextLen[i];
if (start >= textLenUpdate.start) {
lenChanged += textLenUpdate.len;
}
}
text = oldText.substr(0, start + lenChanged) + text + oldText.substr(end + lenChanged);
} else {
start = 0;
end = oldText.length;
}
if (oldText == text) {
return;
}
// Keep track of text len updates
if (oldText.length != text.length) {
this._updatedTextLen.push({
start: start,
end: end,
len: text.length - oldText.length
});
}
this._updatedText = text;
if (this.isTextNode()) {
this.getElem().get(0).textContent = text;
} else if (this.isAttributeNode()) {
this.getElem().attr(this._translateAttribute, text);
} else {
this.getElem().text(text);
}
}
TranslationNode.prototype.highlightText = function() {
if (this.isTextNode()) {
this.getElem().wrap('');
} else {
this.getElem().css('color', 'red');
}
}
TranslationNode.prototype.trim = function (string) {
if (this.isMandatoryTranslate() && string)
return string
return trim(string)
}
/**
* If current node is text node
*/
TranslationNode.prototype.isTextNode = function() {
if (this._isTextNode === undefined) {
this._isTextNode = this._elem.get(0).nodeType == 3;
}
return this._isTextNode;
}
/**
* If current node is comment node
*/
TranslationNode.prototype.isCommentNode = function() {
if (this._isCommentNode === undefined) {
this._isCommentNode = this._elem.get(0).nodeType == 8;
}
return this._isCommentNode;
}
/**
* If current node is a white space or no-content element
*
* @return {boolean}
*/
TranslationNode.prototype.isEmpty = function() {
if (this._isEmpty === undefined) {
// For performance reasons test for space only inline elements
this._isEmpty = this.isInline() && this.trim(this.getText()) == '' && this.getTranslationAttributes().length == 0;
}
return this._isEmpty;
}
/**
* If current node is ignored for translation
* In this case all child nodes are ignored as well
*
* @return {boolean}
*/
TranslationNode.prototype.isIgnored = function() {
if (this._isIgnored === undefined) {
this._isIgnored =
this._elem.is('style,script,noscript,iframe,textarea') ||
this.isEmpty();
}
return this._isIgnored;
}
/**
* If current node should not be translated (because it's a number, date, or has no-translate class)
* If parent has no-translate class, child nodes are not translated as well
* unless overriden by do-translate class.
*
* @return {boolean}
*/
TranslationNode.prototype.isNoTranslate = function() {
if (this._isNoTranslate === undefined) {
this._isNoTranslate =
this.isIgnored() ||
this._elem.hasClass('no-translate') || (
!this._elem.hasClass('do-translate') &&
this._parentNode && this._parentNode.isNoTranslate()
);
}
return this._isNoTranslate;
}
/**
* Check if break translate was requested (translate each text node individualy and not combine into single-piece)
* Requested by using br-translate class on parent node
*
* @return {boolean}
*/
TranslationNode.prototype.isBreakTranslate = function() {
if (this._isBreakTranslate === undefined) {
this._isBreakTranslate =
this._elem.hasClass('br-translate') || (
!this._elem.hasClass('sg-translate') &&
this._parentNode && this._parentNode.isBreakTranslate()
);
}
return this._isBreakTranslate;
}
/**
* Check if single translate was requested by using sg-translate class
*
* @return {boolean}
*/
TranslationNode.prototype.isSingleTranslate = function() {
if (this._isSingleTranslate === undefined) {
this._isSingleTranslate =
this._elem.hasClass('sg-translate') || (
!this.isBreakTranslate() &&
this._parentNode && this._parentNode.isSingleTranslate()
);
}
return this._isSingleTranslate;
}
/**
* If current node is inline, which means it's allowed to be a child inline
* element of a larger text to be translated as a single piece
*
* @return {boolean}
*/
TranslationNode.prototype.isInline = function() {
if (this._isInline === undefined) {
this._isInline =
this.isTextNode() ||
this.isCommentNode() ||
this.isSingleTranslate() || (
!this.isBreakTranslate() &&
this._elem.is('a,b,br,cite,em,i,img,q,s,small,span,strong,sub,sup,u') &&
this._elem.css('display') != 'block'
)
}
return this._isInline;
}
/**
* If all child nodes are inline, current node should be sent for translation as a single piece
*
* @return {boolean}
*/
TranslationNode.prototype.isSinglePiece = function() {
return this._isSinglePiece;
}
/**
* If the current node is a group
*
* @returns {boolean}
*/
TranslationNode.prototype.isNodeNameGroup = function() {
return optionsClass.get('groupNodeNames').includes(this._elem.prop('nodeName'));
}
TranslationNode.prototype.isMandatoryTranslate = function() {
let text = this.getText() ?? ''
for (let regex of optionsClass.get('mandatoryTranslateRegex')) {
if (regex.test(text)) {
return true
}
}
return false
}
/**
* Returns corresponsing jQuery element
*
* @return {jQuery}
*/
TranslationNode.prototype.getElem = function() {
return this._elem;
}
/**
* If current node is an Attribute Node that should be translated
*/
TranslationNode.prototype.isAttributeNode = function() {
return this._translateAttribute !== undefined;
}
/**
* Get non-empty attributes defined for translation using translateAttributes
*
* @return {array} of attribute strings
*/
TranslationNode.prototype.getTranslationAttributes = function() {
if (this._translationAttributes === undefined) {
this._translationAttributes = [];
let translateAttributes = optionsClass.get('translateAttributes')
for (var selector in translateAttributes) {
if ($(this._elem).is(selector)) {
var attrNames = translateAttributes[selector];
for(var i in attrNames) {
var attrName = attrNames[i];
if (this._elem.attr(attrName)) {
this._translationAttributes.push(attrName);
}
}
break;
}
}
}
return this._translationAttributes;
}
/**
* TranslationSentence builds a sentence for translation. It adds all required
* placeholders for no-translate elements
*
* @param {TranslationNode} translationNode single-piece TranslationNode to translate
*/
function TranslationSentence(translationNode, options) {
if (!(this instanceof TranslationSentence)) {
throw new Error('TranslationSentence must be created with the new keyword');
}
if (!translationNode.isSinglePiece() || translationNode.isNoTranslate()) {
throw new Error('TranslationSentence() - single-piece translatable node is expected');
}
this._translationNode = translationNode;
this._options = options;
}
/**
* Translate current sentence
*/
TranslationSentence.prototype.translate = function() {
var text = this.getText();
if (!text) {
return;
}
var translatedText = dictionary.getTranslation(text, this._options.lang);
if (this._options.test) {
translatedText = text;
} else if (!translatedText || translatedText == text) {
return;
}
// Revert previous translation
this._translationNode.revert();
var placeholders = this.getPlaceholders();
if (placeholders.length == 1) {
if (this._options.test) {
placeholders[0].highlightText();
placeholders[0].setText('<-' + translatedText + '->');
} else {
placeholders[0].setText(translatedText);
}
return;
}
for (var i = 0; i < placeholders.length; i++) {
var placeholder = placeholders[i];
var placeholderRegExp = placeholder.getRegExp();
var matches = placeholderRegExp.exec(translatedText);
if (matches) {
if (this._options.test) {
placeholder.highlightText();
placeholder.setText('<-' + matches[1] + '->');
} else {
placeholder.setText(matches[1]);
}
}
}
}
/**
* Get text for translation with all required placeholders
*/
TranslationSentence.prototype.getText = function() {
if (this._text === undefined) {
var placeholders = this.getPlaceholders();
var wrap = placeholders.length > 1; // Add placeholders only if we have more than 1 elements
this._text = placeholders.map(function(placeholder) {
return placeholder.getText(wrap);
}).join(' ');
}
return this._text;
}
/**
* Get array of TextPiece objects to be concatenated into a single sentence for translation
*
* @return {array} of TextPiece objects
*/
TranslationSentence.prototype.getTextPieces = function() {
if (this._textPieces === undefined) {
this._textPieces = [];
var textNodes = this._translationNode.getTextNodes();
for (var i = 0; i < textNodes.length; i++) {
var textNode = textNodes[i];
var text = textNode.getText();
// Match text against ignoreRegex to find any ignore patterns
var match, start = 0;
// If singleTranslate was requested - don't check for regex
if (!textNode.isSingleTranslate() && this._options.dynamicDataPlaceholders) {
while(match = optionsClass.getIgnoreRegex().exec(text)) {
if (match.index > start) {
// Get translatable piece before ignored match
this._textPieces.push(new TextPiece(
textNode, {
start: start,
end: match.index
})
);
}
start = match.index + match[0].length;
// Add ignored match
this._textPieces.push(new TextPiece(
textNode, {
start: match.index,
end: start,
noTranslate: true
})
);
}
}
if (start == 0) {
// No match - add whole element
this._textPieces.push(new TextPiece(textNode));
} else if (start < text.length) {
// Add remaining text piece
this._textPieces.push(new TextPiece(
textNode, {
start: start,
end: text.length,
})
);
}
}
}
return this._textPieces;
}
/**
* Get array of Placeholder objects required to build current sentence
*
* @return {array} of Placeholder objects
*/
TranslationSentence.prototype.getPlaceholders = function() {
if (this._placeHolders === undefined) {
this._placeHolders = [];
var textPieces = this.getTextPieces();
// Ignore no-translate nodes at the beginning of the sentence
var placeholderId = 1;
var ignoreNoTranslate = true;
for (var i = 0; i < textPieces.length; i++) {
var textPiece = textPieces[i];
if (ignoreNoTranslate && textPiece.isNoTranslate()) {
continue;
}
if (textPiece.isNoTranslate()) {
this._placeHolders.push(new Placeholder(placeholderId));
} else {
this._placeHolders.push(new Placeholder(placeholderId, textPiece));
}
// Ignore subsequent noTranslate pieces
ignoreNoTranslate = textPiece.isNoTranslate();
placeholderId++;
}
// Remove all no-translate elements from the end of the sentence
while(this._placeHolders.length && this._placeHolders[this._placeHolders.length - 1].isNoTranslate()) {
this._placeHolders.pop();
}
}
return this._placeHolders;
}
/**
* TextPiece holds text piece for translation
*
* @param {TranslationNode} textNode TranslationNode which holds current text piece
* @param {object} options
*/
function TextPiece(textNode, options) {
if (!(this instanceof TextPiece)) {
throw new Error('TextPiece must be created with the new keyword');
}
if (!textNode.isTextNode() && !textNode.isAttributeNode()) {
throw new Error('TextPiece() - only text nodes or attribute nodes should be added for translation');
}
this._textNode = textNode;
this._options = options || {};
}
/**
* Returns text of the current TextPiece
*
* @return {string}
*/
TextPiece.prototype.getText = function() {
if (this._text === undefined) {
this._text = this._textNode.getText();
if (this._options.start !== undefined && this._options.end !== undefined) {
this._text = this._text.substr(this._options.start, this._options.end - this._options.start);
}
}
return this._text;
}
/**
* Replaces text node with a new (translated) text
*
* @param {string} text
*/
TextPiece.prototype.setText = function(text) {
var trimStartMatches = this.getText().match(optionsClass.getTrimStartRegex());
var trimEndMatches = this.getText().match(optionsClass.getTrimEndRegex());
// Restore whitespace before and after
text = trimStartMatches[1] + text + trimEndMatches[2];
this._textNode.setText(text, this._options.start, this._options.end);
}
TextPiece.prototype.highlightText = function() {
this._textNode.highlightText();
}
/**
* If current TextPiece is a white space-only
*
* @return {boolean}
*/
TextPiece.prototype.isEmpty = function() {
if (this._isEmpty === undefined) {
this._isEmpty = this._textNode.trim(this.getText()) == '';
}
return this._isEmpty;
}
/**
* If current TextPiece shouldn't be translated
*
* @return {boolean}
*/
TextPiece.prototype.isNoTranslate = function() {
if (this._isNoTranslate === undefined) {
this._isNoTranslate =
this._options.noTranslate ||
this._textNode.isNoTranslate() ||
this.isEmpty();
}
return this._isNoTranslate;
}
/**
* If current TextPiece should be translated as a single piece (without checking for ignoreRegex)
*
* @return {boolean}
*/
TextPiece.prototype.isSingleTranslate = function() {
if (this._isSingleTranslate === undefined) {
this._isSingleTranslate =
this._options.singleTranslate ||
this._textNode.isSingleTranslate();
}
return this._isSingleTranslate;
}
/**
* Get corresponding jQuery element
*
* @return {jQuery}
*/
TextPiece.prototype.getElem = function() {
return this._textNode.getElem();
}
/**
* Placeholder holds only translatable text pieces. It may be an empty placeholder
* for no-translate text (this._textPiece will be null)
*
* @param {integer} id id for the current placeholder
* @param {TextPiece} textPiece TextPiece for the current placeholder (may be undefined)
*/
function Placeholder(id, textPiece) {
if (!(this instanceof Placeholder)) {
throw new Error('Placeholder must be created with the new keyword');
}
this._id = id;
this._textPiece = textPiece || null;
}
/**
* Returns text for the current placeholder
*
* @param {boolean} wrap whether to wrap text with placeholder
*
* @return {string}
*/
Placeholder.prototype.getText = function(wrap) {
var textNode = this._textPiece._textNode
var text = this.isNoTranslate() ? '' : textNode.trim(this._textPiece.getText());
if (!wrap) {
return text;
}
if (text) {
return this.getPlaceholderStart() + text + this.getPlaceholderEnd();
} else {
return this.getPlaceholderStart();
}
}
/**
* Replaces text with a new (translated) text
*
* @param {string} text
*/
Placeholder.prototype.setText = function(text) {
if (this._textPiece) {
this._textPiece.setText(text);
}
}
Placeholder.prototype.highlightText = function() {
if (this._textPiece) {
this._textPiece.highlightText();
}
}
/**
* @return {integer} placeholder id
*/
Placeholder.prototype.getId = function() {
return this._id;
}
/**
* @return {RegExp} regex to match placeholder text
*/
Placeholder.prototype.getRegExp = function() {
return new RegExp('\\[P' + this._id + '\\](.*)\\[/P' + this._id + '\\]');
}
/**
* @return {string} placeholder start marker
*/
Placeholder.prototype.getPlaceholderStart = function() {
return '[P' + this._id + ']';
}
/**
* @return {string} placeholder end marker
*/
Placeholder.prototype.getPlaceholderEnd = function() {
return '[/P' + this._id + ']';
}
/**
* If current Placeholder shouldn't be translated
*
* @return {boolean}
*/
Placeholder.prototype.isNoTranslate = function() {
return !this._textPiece || this._textPiece.isNoTranslate();
}
/**
* Translation dictionary
*/
function Dictionary() {
if (!(this instanceof Dictionary)) {
throw new Error('Dictionary must be created with the new keyword');
}
this._missingTranslations = {};
this._pageTexts = {};
}
/**
* @return {string} - translated text
*/
Dictionary.prototype.getTranslation = function(text, lang) {
var cachedDictionary = this.getCachedDictionary(lang);
this._pageTexts[text] = (this._pageTexts[text] || 0) + 1;
if (cachedDictionary.dictionary && cachedDictionary.dictionary[text] !== undefined) {
return cachedDictionary.dictionary[text];
}
this._missingTranslations[lang] = this._missingTranslations[lang] || {};
this._missingTranslations[lang][text] = (this._missingTranslations[lang][text] || 0) + 1;
return null;
}
Dictionary.prototype.loadLang = function(lang, forceLoad) {
var _t = this;
var cachedDictionary = this.getCachedDictionary(lang);
if (cachedDictionary.dictionary && !forceLoad) {
// We already have this lang cached
// When using cache, check if we have newer dictionary version (do it in the background)
window.setTimeout(function() {
_t.checkDictionaryVersion(lang);
}, 3000);
return $.Deferred().resolve();
}
return $.ajax({
url: '/JSTranslate/getDictionary',
method: 'POST',
dataType: 'json',
data: {
lang: lang
}
}).done(function(data) {
_t.updateCachedDictionary(lang, data);
});
}
Dictionary.prototype.redirectLanguageUrl = function(lang) {
var _t = this;
var cachedDictionary = _t.getCachedDictionary(lang);
if (cachedDictionary.meta.subdomain) {
var subdomain = cachedDictionary.meta.subdomain;
var currentUrl = window.location.href;
if (subdomain.slice(-1) !== '/') {
subdomain += '/';
}
if (!currentUrl.startsWith(subdomain)) {
window.location.href = subdomain + currentUrl.substr(currentUrl.lastIndexOf('/') + 1);
}
}
}
Dictionary.prototype.checkDictionaryVersion = function(lang) {
var _t = this;
var cachedDictionary = _t.getCachedDictionary(lang);
if (cachedDictionary.meta.lastVersionCheck) {
var elapsedMs = new Date() - cachedDictionary.meta.lastVersionCheck;
if (elapsedMs < 60 * 1000) {
// Elapsed less than 1 minute, don't perform dictionary version check
return;
}
}
$.ajax({
url: '/JSTranslate/getMeta',
method: 'POST',
dataType: 'json',
data: {
lang: lang
}
}).done(function(data) {
cachedDictionary.meta.lastVersionCheck = + new Date();
_t.updateCachedDictionary(lang, cachedDictionary);
if (data && data.meta && cachedDictionary.meta.updated != data.meta.updated) {
// Force reload dictionary
_t.loadLang(lang, true);
}
});
}
Dictionary.prototype.getCachedDictionary = function(lang) {
if (this._cachedDictionary == undefined) {
this._cachedDictionary = {
meta: {
updated: null,
lastVersionCheck: null,
subdomain: null
}
};
if (!localStorage) {
return this._cachedDictionary;
}
var cachedDictionary = localStorage.getItem('JSTranslateDictionary');
if (!cachedDictionary) {
return this._cachedDictionary;
}
cachedDictionary = JSON.parse(cachedDictionary);
if (!cachedDictionary) {
return this._cachedDictionary;
}
this._cachedDictionary = cachedDictionary;
}
if (lang) {
this._cachedDictionary[lang] = this._cachedDictionary[lang] || {};
return this._cachedDictionary[lang];
} else {
return this._cachedDictionary;
}
}
Dictionary.prototype.updateCachedDictionary = function(lang, data) {
if (!data || !data.meta) {
return;
}
var cachedDictionary = this.getCachedDictionary();
cachedDictionary[lang] = data;
if (!localStorage) {
return;
}
localStorage.setItem('JSTranslateDictionary', JSON.stringify(cachedDictionary));
}
/**
* Sent all sentences without translation to the backend
*
* @param {string} lang
*/
Dictionary.prototype.sendMissingTranslations = function(lang) {
var _t = this;
if (!$.isEmptyObject(_t._missingTranslations[lang])) {
var missingTranslations = JSON.stringify(_t._missingTranslations[lang]);
_t._missingTranslations[lang] = {};
var cachedDictionary = _t.getCachedDictionary(lang);
// If we had missing translations, force reload of cache next time
cachedDictionary.meta.updated = true;
_t.updateCachedDictionary(lang, cachedDictionary);
return $.post('/JSTranslate/missingTranslations', {
page: window.location.href,
lang: lang,
missingTranslations: missingTranslations
}, function() {
_t.loadLang(lang, true);
});
} else {
return $.Deferred().resolve();
}
}
/**
* Show floating div with number of missing translations
*
* @param {string} lang
*/
Dictionary.prototype.showMissingTranslations = function(lang) {
if (!$.isEmptyObject(this._missingTranslations[lang])) {
var newTextsCount = Object.keys(this._missingTranslations[lang]).length;
var newTexts = '
' + Object.keys(this._missingTranslations[lang]).join('
');
showInfo('New texts: ' + newTextsCount + newTexts);
}
//console.log(this._missingTranslations[lang]);
}
/**
* Use MutationObserver to translate dynamically added content
*/
function UpdateListener() {
if (!(this instanceof UpdateListener)) {
throw new Error('UpdateListener must be created with the new keyword');
}
this._rootElem = null;
this._options = null;
this._mutationObserver = null;
this._changeTriggerTimeout = null;
this._pause = false;
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
this._mutationObserver = new MutationObserver(this.mutationHandler.bind(this));
}
}
/**
* @param {object} options
*/
UpdateListener.prototype.setOptions = function(options) {
this._options = options;
}
UpdateListener.prototype.monitor = function(elem) {
var _t = this;
if (!_t._mutationObserver) {
return false;
}
_t._rootElem = elem;
if (!elem) {
_t._mutationObserver.disconnect();
return false;
}
var obsConfig = { childList: true, characterData: true, subtree: true };
$(elem).each(function() {
_t._mutationObserver.observe(this, obsConfig);
});
return true;
}
/**
* MutationObserver handler
*/
UpdateListener.prototype.mutationHandler = function(mutationList) {
if (!this._rootElem) {
return;
}
var translationNeeded = false;
for (var i = 0; i < mutationList.length; i++) {
var mutationRecord = mutationList[i];
if (mutationRecord.type == 'characterData') {
translationNeeded = true;
break;
}
if (mutationRecord.type == 'childList' && mutationRecord.addedNodes.length) {
translationNeeded = true;
break;
}
}
if (translationNeeded) {
// Trigger delayed translation. The delay is needed to collect multiple changes together
this.triggerTranslation(100);
}
}
UpdateListener.prototype.triggerTranslation = function(delay) {
var _t = this;
if (_t._changeTriggerTimeout) {
window.clearTimeout(_t._changeTriggerTimeout);
_t._changeTriggerTimeout = null;
}
if (delay) {
_t._changeTriggerTimeout = window.setTimeout(function() {
_t.triggerTranslation();
_t._changeTriggerTimeout = null;
}, delay);
} else {
if (_t._options && _t._rootElem) {
$(_t._rootElem).JSTranslate(_t._options);
}
}
}
/**
* Override system functions to show translated text
*
* @param {Dictionary} dictionary
*/
function Overrides(dictionary) {
if (!(this instanceof Overrides)) {
throw new Error('Overrides must be created with the new keyword');
}
this._dictionary = dictionary;
this._options = null;
this._overriden = {
alert: window.alert,
confirm: window.confirm
};
window.alert = this.alert.bind(this);
window.confirm = this.confirm.bind(this);
}
/**
* @param {object} options
*/
Overrides.prototype.setOptions = function(options) {
this._options = options;
}
/**
* @param {string} text
*
* @return {string} - translated text
*/
Overrides.prototype.getTranslation = function(text) {
if (!this._options || !this._options.lang) {
return text;
}
var translatedText = this._dictionary.getTranslation(text, this._options.lang);
if (!translatedText) {
if (this._options.showMissing) {
dictionary.showMissingTranslations(this._options.lang);
}
if (this._options.collectMissing) {
dictionary.sendMissingTranslations(this._options.lang);
}
return text;
}
return translatedText;
}
/**
* Override window.alert() function
*/
Overrides.prototype.alert = function(message) {
message = this.getTranslation(message);
return this._overriden.alert.call(window, message);
}
/**
* Override window.confirm() function
*/
Overrides.prototype.confirm = function(message) {
message = this.getTranslation(message);
return this._overriden.confirm.call(window, message);
}
var dictionary = new Dictionary();
var overrides = new Overrides(dictionary);
var updateListener = new UpdateListener();
/**
*
* @param {jQuery} elem
* @param {TranslationNode} parentNode
*/
function createTranslationNode(elem, parentNode) {
var dataName = 'JSTranslateTranslationNode';
var translationNode = $(elem).data(dataName);
if (translationNode) {
translationNode.revert(true);
$(elem).removeData(dataName)
}
translationNode = new TranslationNode(elem, parentNode);
$(elem).data(dataName, translationNode);
// Array of TranslationNode objects which represent child elements of the current node
translationNode._childTranslationNodes = createChildTranslationNodes(translationNode);
return translationNode;
}
/**
* Creates array of child translation nodes
*
* @param {TranslationNode} translationNode
* @return {array}
*/
function createChildTranslationNodes(translationNode) {
if (translationNode.isIgnored() || translationNode.isAttributeNode()) {
return [];
}
var childTranslationNodes = [];
var elem = translationNode.getElem();
$(elem).contents().each(function() {
var childTranslationNode = createTranslationNode($(this), translationNode);
translationNode._isSinglePiece = translationNode._isSinglePiece && childTranslationNode.isInline() && childTranslationNode.isSinglePiece();
childTranslationNodes.push(childTranslationNode);
});
// Add attributes as child nodes
var translationAttributes = translationNode.getTranslationAttributes();
for (var i in translationAttributes) {
var translationAttribute = translationAttributes[i];
var childTranslationNode = new TranslationNode(elem, translationNode);
translationNode._isSinglePiece = false;
translationNode._isEmpty = false;
childTranslationNode._translateAttribute = translationAttribute;
childTranslationNodes.push(childTranslationNode);
}
return childTranslationNodes;
}
/**
* Remove all non-word chars from the beginning and end of the string
*
* @param {string} str
*/
function trim(str) {
return str
.replace(optionsClass.getTrimStartRegex(), '$2')
.replace(optionsClass.getTrimEndRegex(), '$1')
.replace(optionsClass.getIgnoreRegexFull(), '');
}
function showInfo(msg) {
if (!$('#JSTranslateInfo').length) {
var floatDiv = $('