1395 lines
52 KiB
JavaScript
1395 lines
52 KiB
JavaScript
/**
|
|
* @fileOverview Contains the code for jQuery.localizationTool
|
|
*
|
|
* @author Savio Dimatteo <darksmo@gmail.com>
|
|
*/
|
|
|
|
(function($) {
|
|
var _keyboardPressed = false;
|
|
|
|
var methods = {
|
|
/**
|
|
* Returns the ordinal number corresponding to the given language code,
|
|
* or throws in case the given language code is not defined.
|
|
* NOTE: this method operates on the active languages, therefore
|
|
* $this.data('activeLanguageCodeArray') must be available when the
|
|
* method is called.
|
|
*
|
|
* @name _languageCodeToOrdinal
|
|
* @function
|
|
* @access private
|
|
* @param {string} lanuageCode - the language code to convert to ordinal
|
|
* @returns {number} ordinal - the converted ordinal
|
|
*/
|
|
'_languageCodeToOrdinal' : function (languageCode) {
|
|
var $this = this,
|
|
activeLanguageCodes = $this.data('activeLanguageCodeArray');
|
|
|
|
var ordinal = $.inArray(languageCode, activeLanguageCodes);
|
|
|
|
if (ordinal === -1) {
|
|
$.error('Cannot convert ' + languageCode + ' into an ordinal number');
|
|
}
|
|
|
|
return ordinal;
|
|
},
|
|
/**
|
|
* Returns the language code corresponding to the given ordinal number.
|
|
* It throws in case the given ordinal number does not correspond to any
|
|
* language code.
|
|
* NOTE: this method operates on the active languages, therefore
|
|
* $this.data('activeLanguageCodeArray') must be available when the
|
|
* method is called.
|
|
*
|
|
* @name _ordinalToLanguageCode
|
|
* @function
|
|
* @access private
|
|
* @param {number} ordinal - the ordinal number to convert into a language code
|
|
* @returns {string} languageCode - the converted language code
|
|
*/
|
|
'_ordinalToLanguageCode' : function (ordinal) {
|
|
var $this = this,
|
|
activeLanguageCodes = $this.data('activeLanguageCodeArray');
|
|
|
|
if (activeLanguageCodes.length <= ordinal || ordinal < 0) {
|
|
$.error('Cannot convert ' + ordinal + ' into a language code.');
|
|
}
|
|
|
|
return activeLanguageCodes[ordinal];
|
|
},
|
|
/**
|
|
* Returns the html representation for the given language code.
|
|
* @name _languageCodeToHtml
|
|
* @function
|
|
* @param {string} languageCode - the language code as defined in the settings object
|
|
*/
|
|
'_languageCodeToHtml': function (languageCode) {
|
|
var $this = this,
|
|
settings = $this.data('settings'),
|
|
languagesObj = settings.languages,
|
|
languageDefinitionObj = languagesObj[languageCode];
|
|
|
|
var htmlClass = '';
|
|
if (languageDefinitionObj.flag.hasOwnProperty('class')) {
|
|
htmlClass = ' ' + languageDefinitionObj.flag['class'];
|
|
}
|
|
|
|
var htmlImage = '';
|
|
if (languageDefinitionObj.flag.hasOwnProperty('url')) {
|
|
htmlImage = '<img src="' + languageDefinitionObj.flag.url + '" />';
|
|
}
|
|
|
|
var languageName = languageDefinitionObj.language;
|
|
var haveCountry = languageDefinitionObj.hasOwnProperty('country');
|
|
|
|
/*
|
|
* Build up the html
|
|
*/
|
|
var html = [];
|
|
|
|
html.push('<li class="ltool-language ', languageCode, '">');
|
|
|
|
if (settings.showFlag) {
|
|
html.push(
|
|
'<div class="ltool-language-flag', htmlClass, '"></div>'
|
|
);
|
|
html.push(
|
|
htmlImage
|
|
);
|
|
}
|
|
|
|
var interpolatedTemplate = methods._interpolateTemplate.call($this,
|
|
haveCountry ? languageDefinitionObj.country : undefined,
|
|
languageName
|
|
);
|
|
html.push(interpolatedTemplate);
|
|
html.push('</li>');
|
|
|
|
return html.join('');
|
|
},
|
|
/**
|
|
* Interpolates the given country name and language name to the
|
|
* labelTemplate specified in the settings.
|
|
*
|
|
* @param {string} countryName
|
|
* the country name
|
|
* @param {string} languageName
|
|
* the language name
|
|
*
|
|
* @returns {string}
|
|
* the interpolated template
|
|
*/
|
|
_interpolateTemplate: function (countryName, languageName) {
|
|
var $this = this,
|
|
settings = $this.data('settings'),
|
|
template = settings.labelTemplate,
|
|
countryReplacement = '',
|
|
languageReplacement = '',
|
|
haveCountry = typeof countryName === 'string';
|
|
|
|
if (settings.showCountry && haveCountry) {
|
|
countryReplacement = [
|
|
'<span class="ltool-language-country">',
|
|
'$1' + countryName.replace(/[$]/g, '$') + '$2',
|
|
'</span>'
|
|
].join('');
|
|
}
|
|
if (settings.showLanguage) {
|
|
var hasCountryClass = haveCountry ? 'ltool-has-country ' : "";
|
|
languageReplacement = [
|
|
'<span class="', hasCountryClass, 'ltool-language-name">',
|
|
'$1' + languageName.replace(/[$]/g, '$') + '$2' ,
|
|
'</span>'
|
|
].join('');
|
|
}
|
|
|
|
return '<span class="ltool-language-countryname">' +
|
|
template
|
|
.replace(/{{([^{]*)language([^}]*)}}/g, languageReplacement)
|
|
.replace(/{{([^{]*)country([^}]*)}}/g, countryReplacement) +
|
|
'</span>';
|
|
},
|
|
/**
|
|
* Displays the given language in the dropdown menu.
|
|
* @name _selectLanguage
|
|
* @function
|
|
* @access private
|
|
* @param {string} languageCode - the language code
|
|
*/
|
|
'_selectLanguage': function (languageCode) {
|
|
var $this = this;
|
|
|
|
$this.find('.ltool-dropdown-label').html(
|
|
$('.ltool-language.' + languageCode).html()
|
|
);
|
|
|
|
$this.data('selectedLanguageCode', languageCode);
|
|
},
|
|
/**
|
|
* Initializes the localization tool widget.
|
|
* @name _initializeWidget
|
|
* @function
|
|
* @access private
|
|
* @param {array} languageCodeArray - the language code array of the languages to be displayed
|
|
*/
|
|
'_initializeWidget': function (languageCodeArray) {
|
|
|
|
var $this = this,
|
|
settings = $this.data('settings'),
|
|
languagesObj = settings.languages;
|
|
|
|
var markupArray = [];
|
|
|
|
markupArray.push('<span tabindex="0" class="ltool-dropdown-label">Change Language</span><div class="ltool-dropdown-label-arrow"></div>');
|
|
markupArray.push('<ul class="ltool-dropdown-items">');
|
|
var languageCode, i;
|
|
for (i=0;languageCode=languageCodeArray[i++];) {
|
|
|
|
if ( languagesObj.hasOwnProperty(languageCode)) {
|
|
markupArray.push(
|
|
methods._languageCodeToHtml.call($this, languageCode)
|
|
);
|
|
}
|
|
else {
|
|
$.error('The language \'' + languageCode + '\' must be defined');
|
|
}
|
|
}
|
|
markupArray.push('</ul>');
|
|
|
|
$(markupArray.join('')).appendTo($this);
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Handles dropdown click event.
|
|
* @name _onDropdownClicked
|
|
* @function
|
|
* @access private
|
|
*/
|
|
'_onDropdownClicked' : function (/*e*/) {
|
|
var $this = this;
|
|
|
|
var selectedLanguageCode = $this.data('selectedLanguageCode');
|
|
|
|
$this.find('.ltool-language').removeClass('ltool-is-selected');
|
|
$this.find('.' + selectedLanguageCode).addClass('ltool-is-selected');
|
|
|
|
$this.toggleClass('ltool-is-visible');
|
|
|
|
return $this;
|
|
},
|
|
'_closeDropdown' : function () {
|
|
var $this = this;
|
|
|
|
$this.removeClass('ltool-is-visible');
|
|
},
|
|
/**
|
|
* Handles mouseout on dropdown items.
|
|
* @name _onMouseout
|
|
* @function
|
|
* @access private
|
|
*/
|
|
'_onMouseout': function (e) {
|
|
var $this = this;
|
|
|
|
if ($this.find(e.relatedTarget).length > 0) {
|
|
// get rid of the current selected item!
|
|
$this.find('.ltool-is-selected')
|
|
.removeClass('ltool-is-selected');
|
|
|
|
// we will be over an element of ours
|
|
e.preventDefault();
|
|
return $this;
|
|
}
|
|
|
|
/* We will be over another element that doesn't belong to us */
|
|
$this.removeClass('ltool-is-visible');
|
|
},
|
|
/**
|
|
* Handles user clicks on a certain dropdown item.
|
|
* @name _onLanguageSelected
|
|
* @function
|
|
* @param {$element} $item - the jquery item clicked
|
|
* @access private
|
|
*/
|
|
'_onLanguageSelected': function ($item) {
|
|
var $this = this;
|
|
|
|
// extract language code from the $item
|
|
var languageCode = $item.attr('class')
|
|
.replace('ltool-language', '')
|
|
.replace('ltool-is-selected', '')
|
|
.replace(/ /g, '');
|
|
|
|
methods._selectLanguage.call($this, languageCode);
|
|
methods._mayTranslate.call($this, languageCode);
|
|
},
|
|
/**
|
|
* Select the language before the current language in the list.
|
|
* @name _selectPreviousLanguage
|
|
* @function
|
|
* @access private
|
|
*/
|
|
'_selectPreviousLanguage' : function () {
|
|
var $this = this;
|
|
|
|
var currentLanguageCode = $this.data('selectedLanguageCode');
|
|
var currentLanguageCodeOrdinal = methods._languageCodeToOrdinal.call($this, currentLanguageCode);
|
|
|
|
if (currentLanguageCodeOrdinal === 0) {
|
|
return; // cannot go before the first language
|
|
}
|
|
|
|
var nextLanguageCode = methods._ordinalToLanguageCode.call($this, currentLanguageCodeOrdinal-1);
|
|
|
|
// peform the selection
|
|
$this.find('.ltool-is-selected').removeClass('ltool-is-selected');
|
|
methods._selectLanguage.call($this, nextLanguageCode);
|
|
methods._mayTranslate.call($this, nextLanguageCode);
|
|
$this.find('.' + nextLanguageCode).addClass('ltool-is-selected');
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Select the language after the current language in the list.
|
|
* @name _selectPreviousLanguage
|
|
* @function
|
|
* @access private
|
|
*/
|
|
'_selectNextLanguage' : function () {
|
|
var $this = this,
|
|
activeLanguageCodes = $this.data('activeLanguageCodeArray');
|
|
|
|
var currentLanguageCode = $this.data('selectedLanguageCode');
|
|
var currentLanguageCodeOrdinal = methods._languageCodeToOrdinal.call($this, currentLanguageCode);
|
|
|
|
if (currentLanguageCodeOrdinal + 1 >= activeLanguageCodes.length) {
|
|
return;
|
|
}
|
|
|
|
var nextLanguageCode = methods._ordinalToLanguageCode.call($this, currentLanguageCodeOrdinal+1);
|
|
|
|
// peform the selection
|
|
$this.find('.ltool-is-selected').removeClass('ltool-is-selected');
|
|
methods._selectLanguage.call($this, nextLanguageCode);
|
|
methods._mayTranslate.call($this, nextLanguageCode);
|
|
$this.find('.' + nextLanguageCode).addClass('ltool-is-selected');
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Handles keydown event
|
|
* @name _onKeydown
|
|
* @function
|
|
* @param {event} e - the keydown event
|
|
* @access private
|
|
*/
|
|
'_onKeydown': function (e) {
|
|
var $this = this;
|
|
|
|
switch (e.keyCode) {
|
|
case 13: /* enter (open-close menu) */
|
|
methods._onDropdownClicked.call($this);
|
|
e.preventDefault();
|
|
break;
|
|
case 40: /* down (select next) */
|
|
methods._selectNextLanguage.call($this);
|
|
e.preventDefault();
|
|
break;
|
|
case 38: /* up (select previous) */
|
|
methods._selectPreviousLanguage.call($this);
|
|
e.preventDefault();
|
|
break;
|
|
case 27:
|
|
methods._closeDropdown.call($this);
|
|
e.preventDefault();
|
|
break;
|
|
}
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Binds events to the localization tool widget.
|
|
* @name _bindEvents
|
|
* @function
|
|
* @access private
|
|
*/
|
|
'_bindEvents': function () {
|
|
var $this = this;
|
|
|
|
$this
|
|
.bind('mousedown.localizationTool', function (e) {
|
|
_keyboardPressed = false;
|
|
methods._onKeydown.call($this, e);
|
|
})
|
|
.bind('click.localizationTool', function (e) {
|
|
methods._onDropdownClicked.call($this, e);
|
|
})
|
|
.bind('keydown.localizationTool', function (e){
|
|
_keyboardPressed = true;
|
|
methods._onKeydown.call($this, e);
|
|
})
|
|
.bind('mouseout.localizationTool', function (e) {
|
|
//methods._onMouseout.call($this, e);
|
|
})
|
|
.bind('focusout.localizationTool', function () {
|
|
if (_keyboardPressed) {
|
|
methods._closeDropdown.call($this);
|
|
}
|
|
});
|
|
|
|
$this.find('.ltool-language')
|
|
.bind('click.localizationTool', function (/*e*/) {
|
|
methods._onLanguageSelected.call($this, $(this));
|
|
});
|
|
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Analizes the input strings object and decomposes its keys in
|
|
* sections: text strings, id strings, class strings, element strings,
|
|
* attribute strings.
|
|
* @name _decomposeStringsForReferenceMapping
|
|
* @function
|
|
* @access private
|
|
* @returns {object} the decomposition object.
|
|
*/
|
|
'_decomposeStringsForReferenceMapping' : function () {
|
|
var decompositionObj = {
|
|
'idStrings' : [],
|
|
'classStrings' : [],
|
|
'elementStrings' : [],
|
|
'textStrings' : [],
|
|
'attributeStrings' : []
|
|
};
|
|
|
|
var $this = this,
|
|
stringsObj = $this.data('settings').strings;
|
|
|
|
// regexp for attributes matching
|
|
var attrRegexp = new RegExp('^[a-zA-Z-]+?::');
|
|
|
|
var array_string = [];
|
|
var stringKey;
|
|
for (stringKey in stringsObj) {
|
|
if (stringsObj.hasOwnProperty(stringKey)) {
|
|
if (stringKey.match(attrRegexp)) { // NOTE: check first!
|
|
var array_string = stringKey.split(':');
|
|
if($(array_string[3]).length > 0)
|
|
{
|
|
decompositionObj.attributeStrings.push(stringKey);
|
|
}
|
|
}
|
|
else if (stringKey.indexOf('id:') === 0) {
|
|
decompositionObj.idStrings.push(stringKey);
|
|
}
|
|
else if (stringKey.indexOf('class:') === 0) {
|
|
decompositionObj.classStrings.push(stringKey);
|
|
}
|
|
else if (stringKey.indexOf('element:') === 0) {
|
|
decompositionObj.elementStrings.push(stringKey);
|
|
}
|
|
else {
|
|
decompositionObj.textStrings.push(stringKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
return decompositionObj;
|
|
},
|
|
/**
|
|
* Goes through each text node and builds a string reference mapping.
|
|
* It is a mapping (an object)
|
|
* STRING_IDENTIFIER -> <IS_ATTRIBUTE?, ORIGINAL_HTML, [DOM_NODES]>
|
|
* used later for the translation. See init method for a
|
|
* reference. The resulting object is stored internally in
|
|
* $this.data('refMappingObj') as refMapping.
|
|
* @name _buildStringReferenceMapping
|
|
* @function
|
|
* @access private
|
|
*/
|
|
'_buildStringReferenceMapping': function () {
|
|
|
|
var $this = this,
|
|
refMapping = {},
|
|
settings = $this.data('settings'),
|
|
stringsObj = settings.strings;
|
|
|
|
// decompose the initial strings in various bits
|
|
var decompositionObj = methods._decomposeStringsForReferenceMapping.call($this);
|
|
|
|
/*
|
|
* First go through each id
|
|
*/
|
|
|
|
var idString, i;
|
|
for (i=0; idString = decompositionObj.idStrings[i++];) {
|
|
|
|
var idStringName = idString.substring('id:'.length);
|
|
var $idNode = $('#' + idStringName);
|
|
var contents = $idNode.contents();
|
|
|
|
if (settings.ignoreUnmatchedSelectors === true && contents.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (contents.length !== 1) {
|
|
$.error(idString + ' must contain exactly one text node, found ' + contents.length + ' instead');
|
|
}
|
|
else if (contents[0].nodeType !== 3) {
|
|
$.error(idString + ' does not contain a #text node (i.e., type 3)');
|
|
}
|
|
else {
|
|
// add this to the refMapping
|
|
refMapping[idString] = {
|
|
isAttribute : false, // it's an id: selector
|
|
originalText : $idNode.text(),
|
|
domNodes : [ $idNode ]
|
|
};
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helper function to not write the same code over again...
|
|
*/
|
|
var processMultipleElements = function (prefix, jqueryPrefix, checkForIds, checkForClasses) {
|
|
|
|
var string;
|
|
var decompositionKeyPrefix = prefix.replace(':','');
|
|
for (i=0; string = decompositionObj[decompositionKeyPrefix + 'Strings'][i++];) {
|
|
|
|
var stringName = string.substring(prefix.length);
|
|
|
|
// keeps the text of the first dom node in the loop below
|
|
var domNodeText;
|
|
var domNodesArray = [];
|
|
var allNodeTextsAreEqual = true;
|
|
domNodeText = undefined; // note: assigns undefined
|
|
|
|
|
|
var k=0, node;
|
|
NODE:
|
|
for (; node = $(jqueryPrefix + stringName)[k++];) {
|
|
|
|
var $node = $(node);
|
|
|
|
if (checkForIds) {
|
|
var nodeId = $node.attr('id');
|
|
|
|
// skip any node that was previously translated via an id
|
|
if (typeof nodeId === 'string' && stringsObj.hasOwnProperty('id:' + nodeId)) {
|
|
continue NODE;
|
|
}
|
|
}
|
|
|
|
if (checkForClasses) {
|
|
// skip any node that was previously translated via a class
|
|
var nodeClasses = $node.attr('class');
|
|
|
|
if (typeof nodeClasses === 'string') {
|
|
|
|
var nodeClassArray = nodeClasses.split(' '),
|
|
nodeClass,
|
|
j = 0;
|
|
|
|
for(;nodeClass = nodeClassArray[j++];) {
|
|
if (typeof nodeClass === 'string' && stringsObj.hasOwnProperty('class:' + nodeClass)) {
|
|
continue NODE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// make sure this node contains only one text content
|
|
var nodeContents = $node.contents();
|
|
if (nodeContents.length === 0 || nodeContents.length > 1) {
|
|
$.error('A \'' + string + '\' node was found to contain ' + nodeContents.length + ' child nodes. This node must contain exactly one text node!');
|
|
|
|
continue;
|
|
}
|
|
|
|
if (nodeContents[0].nodeType !== 3) {
|
|
$.error('A \'' + string + '\' node does not contain a #text node (i.e., type 3)');
|
|
|
|
continue;
|
|
}
|
|
|
|
// this node is pushable at this point...
|
|
domNodesArray.push($node);
|
|
|
|
// also check the text is the same across the nodes considered
|
|
if (typeof domNodeText === 'undefined') {
|
|
// ... the first time we store the text of the node
|
|
domNodeText = $node.text();
|
|
}
|
|
else if (domNodeText !== $node.text()) {
|
|
// ... then we keep checking if the text node is the same
|
|
allNodeTextsAreEqual = false;
|
|
}
|
|
|
|
} // end for k loop
|
|
|
|
// make sure that the remaining classes contain the same text
|
|
if (!allNodeTextsAreEqual) {
|
|
$.error('Not all text content of elements with ' + string + ' were found to be \'' + domNodeText + '\'. So these elements will be ignored.');
|
|
}
|
|
else {
|
|
// all good
|
|
refMapping[string] = {
|
|
isAttribute : false, // it's a class: or an element: selector
|
|
originalText : domNodeText,
|
|
domNodes : domNodesArray
|
|
};
|
|
}
|
|
}
|
|
|
|
}; // end of processMultipleElements
|
|
|
|
|
|
/*
|
|
* Then go through classes
|
|
*/
|
|
processMultipleElements('class:', '.', true, false);
|
|
|
|
/*
|
|
* Then go through elements
|
|
*/
|
|
processMultipleElements('element:', '', true, true);
|
|
|
|
/*
|
|
* Time to process the attributes
|
|
*/
|
|
var firstSelectorStringRegex = new RegExp('(class|id|element):[^:]');
|
|
var attrString;
|
|
for (i=0; attrString = decompositionObj.attributeStrings[i++];) {
|
|
|
|
|
|
// let's extract the attribute name from the element selector
|
|
var splitStringArray = attrString.split("::");
|
|
var attributeString = splitStringArray.shift();
|
|
|
|
|
|
// sanity check on the format
|
|
if (splitStringArray.length === 0) {
|
|
$.error('sorry, you need to specify class:, element: or id: selectors in ' + attrString);
|
|
}
|
|
|
|
var selectorString = splitStringArray.join('::');
|
|
|
|
if (!splitStringArray[0].match(firstSelectorStringRegex)) {
|
|
$.error(attrString + "Doesn't look right. Perhaps you've added extra semicolons?");
|
|
}
|
|
|
|
|
|
// turn selector into jQuery selector
|
|
selectorString = selectorString.replace('id:', '#');
|
|
selectorString = selectorString.replace('class:', '.');
|
|
selectorString = selectorString.replace('element:', '');
|
|
|
|
// find DOM nodes
|
|
var $domNodes = $(selectorString + '[' + attributeString + ']');
|
|
if ($domNodes.length === 0) {
|
|
$.error('The selector "' + attrString + '" does not point to an existing DOM element');
|
|
}
|
|
|
|
|
|
// avoid using Array.prototype.reduce as it's supported in IE9+
|
|
var j = 0,
|
|
allSameAttributeValue = true;
|
|
|
|
var attributeText = $($domNodes[0]).attr(attributeString);
|
|
|
|
var domNodesToAdd = [];
|
|
|
|
for (j=0; j<$domNodes.length; j++) {
|
|
// check the placeholder text is all the same
|
|
var $dom = $($domNodes[j]);
|
|
if (attributeText !== $dom.attr(attributeString)) {
|
|
allSameAttributeValue = false;
|
|
}
|
|
|
|
// also add for later...
|
|
domNodesToAdd.push($dom);
|
|
}
|
|
if (!allSameAttributeValue) {
|
|
$.error('Not all the attribute values selected via ' + attrString + ' are the same');
|
|
}
|
|
|
|
// now we have everything in place, we just add it to the rest!
|
|
refMapping[attrString] = {
|
|
isAttribute : true, // yes, we are dealing with an attribute here
|
|
originalText : attributeText,
|
|
domNodes : domNodesToAdd
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* Finally find the dom nodes associated to any text searched
|
|
*/
|
|
var textString;
|
|
|
|
for (i=0; textString = decompositionObj.textStrings[i++];) {
|
|
// nodes that will contain the text to translate
|
|
var textNodesToAdd = [];
|
|
|
|
var allParentNodes = $(':contains(' + textString + ')');
|
|
var k, parentNode;
|
|
for (k=0; parentNode = allParentNodes[k++];) {
|
|
var nodeContents = $(parentNode).contents();
|
|
if (nodeContents.length === 1 &&
|
|
nodeContents[0].nodeType === 3) {
|
|
|
|
textNodesToAdd.push($(parentNode));
|
|
}
|
|
}
|
|
if (textNodesToAdd.length > 0) {
|
|
// all good
|
|
refMapping[textString] = {
|
|
isAttribute: false, // no it's just another dom element
|
|
originalText : textString,
|
|
domNodes : textNodesToAdd
|
|
};
|
|
}
|
|
}
|
|
|
|
$this.data('refMappingObj', refMapping);
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Calls the user specified callback (if any), then translates the page.
|
|
* If the user returned 'false' in his/her callback, the translation is
|
|
* not performed.
|
|
* @name _mayTranslate
|
|
* @function
|
|
* @access private
|
|
* @param {string} [languageCode] - the language code to translate to
|
|
*/
|
|
'_mayTranslate': function (languageCode) {
|
|
var $this = this,
|
|
settings = $this.data('settings');
|
|
|
|
if (false !== settings.onLanguageSelected(languageCode)) {
|
|
methods._translate.call($this, languageCode);
|
|
}
|
|
},
|
|
/**
|
|
* Returns the code of the language currently selected
|
|
* @name getSelectedLanguageCode
|
|
* @function
|
|
* @access public
|
|
* @returns {string} [languageCode] - the language code currently selected
|
|
*/
|
|
'getSelectedLanguageCode' : function () {
|
|
var $this = this;
|
|
return $this.data('selectedLanguageCode');
|
|
},
|
|
/**
|
|
* Translates the current page.
|
|
* @name translate
|
|
* @function
|
|
* @access public
|
|
* @param {string} [languageCode] - the language to translate to.
|
|
*/
|
|
'_translate': function (languageCode) {
|
|
var $this = this,
|
|
settings = $this.data('settings'),
|
|
stringsObj = settings.strings,
|
|
refMappingObj = $this.data('refMappingObj');
|
|
|
|
var cssDirection = 'ltr';
|
|
if (typeof languageCode !== 'undefined') {
|
|
// check if the language code exists actually
|
|
if (!settings.languages.hasOwnProperty(languageCode)) {
|
|
$.error('The language code ' + languageCode + ' is not defined');
|
|
return $this;
|
|
}
|
|
|
|
// check if we are dealing with a right to left language
|
|
if (settings.languages[languageCode].hasOwnProperty('cssDirection')) {
|
|
|
|
cssDirection = settings.languages[languageCode].cssDirection;
|
|
}
|
|
}
|
|
|
|
// translate everything according to the reference mapping
|
|
var string;
|
|
for (string in refMappingObj) {
|
|
if (refMappingObj.hasOwnProperty(string)) {
|
|
|
|
// get the translation for this string...
|
|
var translation;
|
|
if (typeof languageCode === 'undefined' || languageCode === settings.defaultLanguage) {
|
|
translation = refMappingObj[string].originalText;
|
|
}
|
|
else {
|
|
translation = stringsObj[string][languageCode];
|
|
}
|
|
|
|
var domNodes = refMappingObj[string].domNodes;
|
|
|
|
var $domNode, i;
|
|
|
|
// attribute case
|
|
if (refMappingObj[string].isAttribute === true) {
|
|
var attributeName = string.split("::", 1)[0];
|
|
|
|
for (i=0; $domNode = domNodes[i++];) {
|
|
$domNode.attr(attributeName, translation);
|
|
$domNode.css('direction', cssDirection);
|
|
}
|
|
|
|
}
|
|
else {
|
|
// all other cases
|
|
for (i=0; $domNode = domNodes[i++];) {
|
|
$domNode.html(translation);
|
|
$domNode.css('direction', cssDirection);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Translates according to the widget configuration programmatically.
|
|
* This is meant to be called by the user. The difference with the
|
|
* private counterpart _translate method is that the language is
|
|
* selected in the widget.
|
|
*/
|
|
'translate' : function (languageCode) {
|
|
var $this = this;
|
|
|
|
methods._translate.call($this, languageCode);
|
|
|
|
// must also select the language when translating via the public method
|
|
methods._selectLanguage.call($this, languageCode);
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Translates a given string.
|
|
*
|
|
* @param {string} textToTranslate
|
|
* A string of text to be translated. Requires the string is defined
|
|
* during plugin initialization.
|
|
* @param {string} languageCode
|
|
* A known language code like 'de_DE'
|
|
*
|
|
* @returns {string}
|
|
* The translated string. The method throws an error if it was not
|
|
* possible to translate the string.
|
|
*/
|
|
'translateString' : function (textToTranslate, languageCode) {
|
|
var $this = this;
|
|
var settings = $this.data('settings');
|
|
var stringsObj = settings.strings;
|
|
|
|
if (!settings.languages.hasOwnProperty(languageCode)) {
|
|
$.error('The language code ' + languageCode + ' is not known');
|
|
return;
|
|
}
|
|
|
|
if (!stringsObj.hasOwnProperty(textToTranslate)) {
|
|
$.error('The string \'' + textToTranslate + '\' was not translated in any language.');
|
|
return;
|
|
}
|
|
|
|
var oTranslations = stringsObj[textToTranslate] || {};
|
|
if (!oTranslations.hasOwnProperty(languageCode)) {
|
|
$.error('A translation for the string \'' + textToTranslate + '\' was not defined for language ' + languageCode + ". Defined languages are: " + Object.keys(oTranslations).join(", "));
|
|
return;
|
|
}
|
|
|
|
return oTranslations[languageCode];
|
|
},
|
|
/**
|
|
* Destroys the dropdown widget.
|
|
*
|
|
* @name destroy
|
|
* @function
|
|
* @access public
|
|
**/
|
|
'destroy' : function () {
|
|
var $this = this;
|
|
|
|
// remove all data set with .data()
|
|
$this.removeData();
|
|
|
|
// unbind events
|
|
$this.unbind('click.localizationTool', function (e) {
|
|
methods._onDropdownClicked.call($this, e);
|
|
});
|
|
$this.find('.ltool-language')
|
|
.unbind('click.localizationTool', function (/*e*/) {
|
|
methods._onLanguageSelected.call($this, $(this));
|
|
});
|
|
|
|
$this
|
|
.unbind('mouseout.localizationTool', function (e) {
|
|
methods._onMouseout.call($this, e);
|
|
});
|
|
|
|
// remove markup
|
|
$this.empty();
|
|
|
|
return $this;
|
|
},
|
|
/**
|
|
* Sorts the given array of countryLanguageCodes by country name.
|
|
* If a language has no name goes to the bottom of the list.
|
|
*
|
|
* @name _sortCountryLanguagesByCountryName
|
|
* @function
|
|
* @access private
|
|
* @param {object} languagesDefinition - the array countryLanguageCodes defined during initialization.
|
|
* @param {array} arrayOfCountryLanguageCodes - the input array countryLanguageCodes.
|
|
* @returns {array} sortedArrayOfCountryLanguageCodes - the sorted array countryLanguageCodes.
|
|
*/
|
|
'_sortCountryLanguagesByCountryName': function (languagesDefinition, arrayOfCountryLanguageCodes) {
|
|
return arrayOfCountryLanguageCodes.sort(function (a, b) {
|
|
if (languagesDefinition[a].hasOwnProperty('country') && languagesDefinition[b].hasOwnProperty('country')) {
|
|
return languagesDefinition[a].country.localeCompare(
|
|
languagesDefinition[b].country
|
|
);
|
|
}
|
|
else if (languagesDefinition[a].hasOwnProperty('country')) {
|
|
return languagesDefinition[a].country.localeCompare(
|
|
languagesDefinition[b].language
|
|
);
|
|
}
|
|
// else if (languagesDefinition[b].hasOwnProperty('country')) {
|
|
return languagesDefinition[a].language.localeCompare(
|
|
languagesDefinition[b].country
|
|
);
|
|
// }
|
|
});
|
|
},
|
|
/**
|
|
* Goes through each string defined and extracts the common subset of
|
|
* languages that actually used. The default language is added to this
|
|
* subset a priori. The resulting list is sorted by country name.
|
|
*
|
|
* @name _findSubsetOfUsedLanguages
|
|
* @function
|
|
* @access private
|
|
* @param {object} stringsObj - the strings to translate
|
|
* @returns {array} usedLanguageCodes - an array of country codes sorted based on country names.
|
|
*/
|
|
'_findSubsetOfUsedLanguages' : function (stringsObj) {
|
|
var $this = this;
|
|
var string;
|
|
var settings = $this.data('settings');
|
|
|
|
// build an histogram of all the used languages in strings
|
|
var usedLanguagesHistogram = {};
|
|
var howManyDifferentStrings = 0;
|
|
|
|
for (string in stringsObj) {
|
|
if (stringsObj.hasOwnProperty(string)) {
|
|
|
|
var languages = stringsObj[string],
|
|
language;
|
|
|
|
for (language in languages) {
|
|
if (languages.hasOwnProperty(language)) {
|
|
if (!usedLanguagesHistogram.hasOwnProperty(language)) {
|
|
usedLanguagesHistogram[language] = 0;
|
|
}
|
|
}
|
|
usedLanguagesHistogram[language]++;
|
|
}
|
|
|
|
howManyDifferentStrings++;
|
|
}
|
|
}
|
|
|
|
// find languages that are guaranteed to appear in all strings
|
|
var guaranteedLanguages = [],
|
|
languageCode;
|
|
|
|
for (languageCode in usedLanguagesHistogram) {
|
|
if (usedLanguagesHistogram.hasOwnProperty(languageCode) &&
|
|
usedLanguagesHistogram[languageCode] === howManyDifferentStrings
|
|
) {
|
|
|
|
guaranteedLanguages.push(languageCode);
|
|
}
|
|
}
|
|
|
|
// delete the default language if it's in the guaranteed languages
|
|
var defaultIdx = $.inArray(settings.defaultLanguage, guaranteedLanguages);
|
|
if (defaultIdx > -1) {
|
|
// delete the default language from the array
|
|
guaranteedLanguages.splice(defaultIdx, 1);
|
|
}
|
|
|
|
// add the default language in front
|
|
guaranteedLanguages.unshift(settings.defaultLanguage);
|
|
|
|
return methods._sortCountryLanguagesByCountryName.call(
|
|
this,
|
|
settings.languages,
|
|
guaranteedLanguages
|
|
);
|
|
},
|
|
/**
|
|
* Initialises the localization tool plugin.
|
|
* @name init
|
|
* @function
|
|
* @param {object} [options] - the user options
|
|
* @access public
|
|
* @returns jqueryObject
|
|
*/
|
|
'init' : function(options) {
|
|
// NOTE: "country" is optional
|
|
var knownLanguages = {
|
|
'en_GB' : {
|
|
'country' : 'United Kingdom',
|
|
'language': 'English',
|
|
'countryTranslated' : 'United Kingdom',
|
|
'languageTranslated': 'English',
|
|
'flag': {
|
|
'class' : 'flag flag-gb'
|
|
}
|
|
},
|
|
'de_DE' : {
|
|
'country' : 'Germany',
|
|
'language' : 'German',
|
|
'countryTranslated' : 'Deutschland',
|
|
'languageTranslated' : 'Deutsch',
|
|
'flag' : {
|
|
'class' : 'flag flag-de'
|
|
}
|
|
},
|
|
'es_ES' : {
|
|
'country' : 'Spain',
|
|
'language' : 'Spanish',
|
|
'countryTranslated': 'España',
|
|
'languageTranslated' : 'Español',
|
|
'flag' : {
|
|
'class' : 'flag flag-es'
|
|
}
|
|
},
|
|
'fr_FR' : {
|
|
'country' : 'France',
|
|
'language' : 'French',
|
|
'countryTranslated' : 'France',
|
|
'languageTranslated' : 'Français',
|
|
'flag' : {
|
|
'class' : 'flag flag-fr'
|
|
}
|
|
},
|
|
'ko_KR' : {
|
|
'country' : 'Korea, Republic of.',
|
|
'language' : 'Korean',
|
|
'countryTranslated' : '대한민국',
|
|
'languageTranslated' : '한국어',
|
|
'flag' : {
|
|
'class' : 'flag flag-kr'
|
|
}
|
|
},
|
|
'pt_BR' : {
|
|
'country' : 'Brazil',
|
|
'language' : 'Portuguese',
|
|
'countryTranslated': 'Brasil',
|
|
'languageTranslated' : 'Português',
|
|
'flag' : {
|
|
'class' : 'flag flag-br'
|
|
}
|
|
},
|
|
'en_AU' : {
|
|
'country' : 'Australia',
|
|
'language' : 'English',
|
|
'countryTranslated' : 'Australia',
|
|
'languageTranslated' : 'English',
|
|
'flag' : {
|
|
'class' : 'flag flag-au'
|
|
}
|
|
},
|
|
'en_IN' : {
|
|
'country' : 'India',
|
|
'language' : 'English',
|
|
'countryTranslated': 'India',
|
|
'languageTranslated': 'English',
|
|
'flag': {
|
|
'class' : 'flag flag-in'
|
|
}
|
|
},
|
|
'it_IT' : {
|
|
'country' : 'Italy',
|
|
'language': 'Italian',
|
|
'countryTranslated': 'Italia',
|
|
'languageTranslated': 'Italiano',
|
|
'flag' : {
|
|
'class' : 'flag flag-it'
|
|
}
|
|
},
|
|
'jp_JP' : {
|
|
'country' : 'Japan',
|
|
'language': 'Japanese',
|
|
'countryTranslated': '日本',
|
|
'languageTranslated': '日本語',
|
|
'flag' : {
|
|
'class' : 'flag flag-jp'
|
|
}
|
|
},
|
|
'ar_TN' : {
|
|
'country' : 'Tunisia',
|
|
'language' : 'Arabic',
|
|
'countryTranslated': 'تونس',
|
|
'languageTranslated': 'عربي',
|
|
'cssDirection': 'rtl',
|
|
'flag' : {
|
|
'class' : 'flag flag-tn'
|
|
}
|
|
},
|
|
'el_GR' : {
|
|
'country': 'Greece',
|
|
'language': 'Greek',
|
|
'countryTranslated': 'Ελλάδα',
|
|
'languageTranslated' : 'Ελληνικά',
|
|
'flag' : {
|
|
'class' : 'flag flag-gr'
|
|
}
|
|
},
|
|
'en_IE' : {
|
|
'country': 'Ireland',
|
|
'language': 'English',
|
|
'countryTranslated': 'Ireland',
|
|
'languageTranslated' : 'English',
|
|
'flag' : {
|
|
'class' : 'flag flag-ie'
|
|
}
|
|
},
|
|
'nl_NL': {
|
|
'country' : 'Netherlands',
|
|
'language': 'Dutch',
|
|
'countryTranslated' : 'Nederland',
|
|
'languageTranslated' : 'Nederlands',
|
|
'flag' : {
|
|
'class' : 'flag flag-nl'
|
|
}
|
|
},
|
|
'zh_CN': {
|
|
'country' : 'China',
|
|
'language' : 'Simplified Chinese',
|
|
'countryTranslated': '中国',
|
|
'languageTranslated': '简体中文',
|
|
'flag' : {
|
|
'class' : 'flag flag-cn'
|
|
}
|
|
},
|
|
'zh_TW': {
|
|
'country' : 'Taiwan',
|
|
'language' : 'Traditional Chinese',
|
|
'countryTranslated': '臺灣',
|
|
'languageTranslated': '繁體中文',
|
|
'flag' : {
|
|
'class' : 'flag flag-tw'
|
|
}
|
|
},
|
|
'fi_FI': {
|
|
'country' : 'Finland',
|
|
'language' : 'Finnish',
|
|
'countryTranslated' : 'Suomi',
|
|
'languageTranslated' : 'Suomi',
|
|
'flag' : {
|
|
'class' : 'flag flag-fi'
|
|
}
|
|
},
|
|
'pt_PT' : {
|
|
'country' : 'Portugal',
|
|
'language' : 'Portuguese',
|
|
'countryTranslated': 'Portugal',
|
|
'languageTranslated' : 'Português',
|
|
'flag' : {
|
|
'class' : 'flag flag-pt'
|
|
}
|
|
},
|
|
'pl_PL': {
|
|
'country' : 'Poland',
|
|
'language': 'Polish',
|
|
'countryTranslated' : 'Polska',
|
|
'languageTranslated': 'Polski',
|
|
'flag' : {
|
|
'class' : 'flag flag-pl'
|
|
}
|
|
},
|
|
'ru_RU': {
|
|
'country' : 'Russia',
|
|
'language' : 'Russian',
|
|
'languageTranslated': 'Русский',
|
|
'countryTranslated' : 'Россия',
|
|
'flag': {
|
|
'class': 'flag flag-ru'
|
|
}
|
|
},
|
|
'hi_IN': {
|
|
'country' : 'India',
|
|
'language': 'Hindi',
|
|
'countryTranslated': 'भारत',
|
|
'languageTranslated': 'हिन्द',
|
|
'flag': {
|
|
'class': 'flag flag-in'
|
|
}
|
|
},
|
|
'ta_IN': {
|
|
'country' : 'India',
|
|
'language': 'Tamil',
|
|
'countryTranslated': 'இந்தியா',
|
|
'languageTranslated': 'தமிழ்',
|
|
'flag': {
|
|
'class': 'flag flag-in'
|
|
}
|
|
},
|
|
'ml_IN': {
|
|
'country' : 'India',
|
|
'language': 'Malayalam',
|
|
'countryTranslated': 'ഇന്ത്യ',
|
|
'languageTranslated': 'മലയാളം',
|
|
'flag': {
|
|
'class': 'flag flag-in'
|
|
}
|
|
},
|
|
'tr_TR': {
|
|
'country' : 'Turkey',
|
|
'language' : 'Turkish',
|
|
'countryTranslated': 'Türkiye',
|
|
'languageTranslated': 'Türkçe',
|
|
'flag': {
|
|
'class': 'flag flag-tr'
|
|
}
|
|
},
|
|
'he_IL': {
|
|
'country' : 'Israel',
|
|
'language' : 'Hebrew',
|
|
'countryTranslated' : 'מדינת ישראל',
|
|
'languageTranslated': 'עברית',
|
|
'cssDirection': 'rtl',
|
|
'flag': {
|
|
'class': 'flag flag-il'
|
|
}
|
|
},
|
|
'da_DK' : {
|
|
'country' : 'Denmark',
|
|
'language' : 'Danish',
|
|
'countryTranslated': 'Danmark',
|
|
'languageTranslated': 'Dansk',
|
|
'flag' : {
|
|
'class': 'flag flag-dk'
|
|
}
|
|
},
|
|
'ro_RO': {
|
|
'country' : 'Romania',
|
|
'language' : 'Romanian',
|
|
'countryTranslated': 'România',
|
|
'languageTranslated': 'Român',
|
|
'flag' : {
|
|
'class': 'flag flag-ro'
|
|
}
|
|
},
|
|
'eo' : {
|
|
// NOTE: no country
|
|
'language' : 'Esperanto',
|
|
'languageTranslated' : 'Esperanto',
|
|
'flag' : {
|
|
'class': 'flag flag-esperanto'
|
|
}
|
|
},
|
|
'sk_SK' : {
|
|
'country' : 'Slovakia',
|
|
'language' : 'Slovak',
|
|
'countryTranslated': 'Slovensko',
|
|
'languageTranslated': 'Slovenčina',
|
|
'flag' : {
|
|
'class': 'flag flag-sk'
|
|
}
|
|
},
|
|
'es_MX' : {
|
|
'country' : 'Mexico',
|
|
'language' : 'Spanish',
|
|
'countryTranslated': 'México',
|
|
'languageTranslated': 'Español',
|
|
'flag' : {
|
|
'class': 'flag flag-mx'
|
|
}
|
|
},
|
|
'tl_PH' : {
|
|
'country' : 'Philippines',
|
|
'language' : 'Tagalog',
|
|
'countryTranslated': 'Pilipinas',
|
|
'languageTranslated': 'Tagalog',
|
|
'flag' : {
|
|
'class': 'flag flag-ph'
|
|
}
|
|
},
|
|
'cs_CZ' : {
|
|
'country' : 'Czech Republic',
|
|
'language' : 'Czech',
|
|
'countryTranslated': 'Česká republika',
|
|
'languageTranslated': 'Čeština',
|
|
'flag' : {
|
|
'class': 'flag flag-cz'
|
|
}
|
|
}
|
|
};
|
|
|
|
var settings = $.extend({
|
|
'defaultLanguage' : 'en_GB',
|
|
/* do not throw error if a selector doesn't match */
|
|
'ignoreUnmatchedSelectors': false,
|
|
/* show the flag on the widget */
|
|
'showFlag' : true,
|
|
/* show the language on the widget */
|
|
'showLanguage': true,
|
|
/* show the country on the widget */
|
|
'showCountry': true,
|
|
/* format of the language/country label */
|
|
'labelTemplate': '{{country}} {{(language)}}',
|
|
'languages' : {
|
|
/*
|
|
* The format here is <country code>_<language code>.
|
|
* - list of country codes: http://www.gnu.org/software/gettext/manual/html_node/Country-Codes.html
|
|
* - list of language codes: http://www.gnu.org/software/gettext/manual/html_node/Usual-Language-Codes.html#Usual-Language-Codes
|
|
*/
|
|
},
|
|
/*
|
|
* Strings are provided by the user of the plugin. Each entry
|
|
* in the dictionary has the form:
|
|
*
|
|
* [STRING_IDENTIFIER] : {
|
|
* [LANGUAGE] : [TRANSLATION]
|
|
* }
|
|
*
|
|
* STRING_IDENTIFIER:
|
|
* id:<html-id-name> OR
|
|
* class:<html-class-name> OR
|
|
* element:<html-element-name> OR
|
|
* <string>
|
|
*
|
|
* LANGUAGE: one of the languages defined above (e.g., it_IT)
|
|
*
|
|
* TRANSLATION: <string>
|
|
*
|
|
*/
|
|
'strings' : {},
|
|
/*
|
|
* A callback called whenever the user selects the language
|
|
* from the dropdown menu. If false is returned, the
|
|
* translation will not be performed (but just the language
|
|
* will be selected from the widget).
|
|
*
|
|
* The countryLanguageCode is a string representing the
|
|
* selected language identifier like 'en_GB'
|
|
*/
|
|
'onLanguageSelected' : function (/*countryLanguageCode*/) { return true; }
|
|
}, options);
|
|
|
|
// add more languages
|
|
settings.languages = $.extend(knownLanguages, settings.languages);
|
|
|
|
// check that the default language is defined
|
|
if (!settings.languages.hasOwnProperty(settings.defaultLanguage)) {
|
|
$.error('FATAL: the default language ' + settings.defaultLanguage + ' is not defined in the \'languages\' parameter!');
|
|
}
|
|
|
|
return this.each(function() {
|
|
// save settings
|
|
var $this = $(this);
|
|
|
|
$this.data('settings', settings);
|
|
|
|
// language codes common to all translations
|
|
var activeLanguageCodeArray = methods._findSubsetOfUsedLanguages.call(
|
|
$this, settings.strings
|
|
);
|
|
$this.data('activeLanguageCodeArray', activeLanguageCodeArray);
|
|
|
|
methods._initializeWidget.call($this, activeLanguageCodeArray);
|
|
|
|
methods._selectLanguage.call($this, settings.defaultLanguage);
|
|
|
|
methods._bindEvents.call($this);
|
|
|
|
methods._buildStringReferenceMapping.call($this);
|
|
});
|
|
}
|
|
};
|
|
|
|
var __name__ = 'localizationTool';
|
|
|
|
/**
|
|
* jQuery Localization Tool - a jQuery widget to translate web pages
|
|
*
|
|
* @memberOf jQuery.fn
|
|
*/
|
|
$.fn[__name__] = function(method) {
|
|
/*
|
|
* Just a router for method calls
|
|
*/
|
|
if (methods[method]) {
|
|
if (this.data('initialized') === true) {
|
|
// call a method
|
|
return methods[method].apply(this,
|
|
Array.prototype.slice.call(arguments, 1)
|
|
);
|
|
}
|
|
else {
|
|
throw new Error('method ' + method + ' called on an uninitialized instance of ' + __name__);
|
|
}
|
|
}
|
|
else if (typeof method === 'object' || !method) {
|
|
// call init, user passed the settings as parameters
|
|
this.data('initialized', true);
|
|
return methods.init.apply(this, arguments);
|
|
}
|
|
else {
|
|
$.error('Cannot call method ' + method);
|
|
}
|
|
};
|
|
})(jQuery);
|