jquery-lang.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. /*
  2. The MIT License (MIT)
  3. Copyright (c) 2014 Irrelon Software Limited
  4. http://www.irrelon.com
  5. Permission is hereby granted, free of charge, to any person obtaining a copy
  6. of this software and associated documentation files (the "Software"), to deal
  7. in the Software without restriction, including without limitation the rights
  8. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. copies of the Software, and to permit persons to whom the Software is
  10. furnished to do so, subject to the following conditions:
  11. The above copyright notice, url and this permission notice shall be included in
  12. all copies or substantial portions of the Software.
  13. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19. THE SOFTWARE.
  20. Source: https://github.com/coolbloke1324/jquery-lang-js
  21. Changelog:
  22. Version 2.0.0 - Complete re-write.
  23. */
  24. var Lang = (function () {
  25. var Lang = function (defaultLang, currentLang, allowCookieOverride) {
  26. var self = this,
  27. cookieLang;
  28. // Enable firing events
  29. this._fireEvents = true;
  30. // Allow storage of dynamic language pack data
  31. this._dynamic = {};
  32. // Store existing mutation methods so we can auto-run
  33. // translations when new data is added to the page
  34. this._mutationCopies = {
  35. append: $.fn.append,
  36. appendTo: $.fn.appendTo,
  37. prepend: $.fn.prepend,
  38. before: $.fn.before,
  39. after: $.fn.after,
  40. html: $.fn.html
  41. };
  42. // Now override the existing mutation methods with our own
  43. $.fn.append = function () { return self._mutation(this, 'append', arguments) };
  44. $.fn.appendTo = function () { return self._mutation(this, 'appendTo', arguments) };
  45. $.fn.prepend = function () { return self._mutation(this, 'prepend', arguments) };
  46. $.fn.before = function () { return self._mutation(this, 'before', arguments) };
  47. $.fn.after = function () { return self._mutation(this, 'after', arguments) };
  48. $.fn.html = function () { return self._mutation(this, 'html', arguments) };
  49. // Set default and current language to the default one
  50. // to start with
  51. this.defaultLang = defaultLang || 'en';
  52. this.currentLang = defaultLang || 'en';
  53. // Check for cookie support when no current language is specified
  54. if ((allowCookieOverride || !currentLang) && $.cookie) {
  55. // Check for an existing language cookie
  56. cookieLang = $.cookie('langCookie');
  57. if (cookieLang) {
  58. // We have a cookie language, set the current language
  59. currentLang = cookieLang;
  60. }
  61. }
  62. $(function () {
  63. // Setup data on the language items
  64. self._start();
  65. // Check if the current language is not the same as our default
  66. if (currentLang && currentLang !== self.defaultLang) {
  67. // Switch to the current language
  68. self.change(currentLang);
  69. }
  70. })
  71. };
  72. /**
  73. * Object that holds the language packs.
  74. * @type {{}}
  75. */
  76. Lang.prototype.pack = {};
  77. /**
  78. * Array of translatable attributes to check for on elements.
  79. * @type {string[]}
  80. */
  81. Lang.prototype.attrList = [
  82. 'title',
  83. 'alt',
  84. 'placeholder'
  85. ];
  86. /**
  87. * Defines a language pack that can be dynamically loaded and the
  88. * path to use when doing so.
  89. * @param {String} lang The language two-letter iso-code.
  90. * @param {String} path The path to the language pack js file.
  91. */
  92. Lang.prototype.dynamic = function (lang, path) {
  93. if (lang !== undefined && path !== undefined) {
  94. this._dynamic[lang] = path;
  95. }
  96. };
  97. /**
  98. * Loads a new language pack for the given language.
  99. * @param {string} lang The language to load the pack for.
  100. * @param {Function=} callback Optional callback when the file has loaded.
  101. */
  102. Lang.prototype.loadPack = function (lang, callback) {
  103. var self = this;
  104. if (lang && self._dynamic[lang]) {
  105. $.ajax({
  106. dataType: "json",
  107. url: self._dynamic[lang],
  108. success: function (data) {
  109. self.pack[lang] = data;
  110. // Process the regex list
  111. if (self.pack[lang].regex) {
  112. var packRegex = self.pack[lang].regex,
  113. regex,
  114. i;
  115. for (i = 0; i < packRegex.length; i++) {
  116. regex = packRegex[i];
  117. if (regex.length === 2) {
  118. // String, value
  119. regex[0] = new RegExp(regex[0]);
  120. } else if (regex.length === 3) {
  121. // String, modifiers, value
  122. regex[0] = new RegExp(regex[0], regex[1]);
  123. // Remove modifier
  124. regex.splice(1, 1);
  125. }
  126. }
  127. }
  128. console.log('Loaded language pack: ' + self._dynamic[lang]);
  129. if (callback) { callback(false, lang, self._dynamic[lang]); }
  130. },
  131. error: function () {
  132. console.log('Error loading language pack' + self._dynamic[lang]);
  133. if (callback) { callback(true, lang, self._dynamic[lang]); }
  134. }
  135. });
  136. } else {
  137. throw('Cannot load language pack, no file path specified!');
  138. }
  139. };
  140. /**
  141. * Scans the DOM for elements with [lang] selector and saves translate data
  142. * for them for later use.
  143. * @private
  144. */
  145. Lang.prototype._start = function (selector) {
  146. // Get the page HTML
  147. var arr = selector !== undefined ? $(selector).find('[lang]') : $(':not(html)[lang]'),
  148. arrCount = arr.length,
  149. elem;
  150. while (arrCount--) {
  151. elem = $(arr[arrCount]);
  152. this._processElement(elem);
  153. }
  154. };
  155. Lang.prototype._processElement = function (elem) {
  156. // Only store data if the element is set to our default language
  157. if (elem.attr('lang') === this.defaultLang) {
  158. // Store translatable attributes
  159. this._storeAttribs(elem);
  160. // Store translatable content
  161. this._storeContent(elem);
  162. }
  163. };
  164. /**
  165. * Stores the translatable attribute values in their default language.
  166. * @param {object} elem The jQuery selected element.
  167. * @private
  168. */
  169. Lang.prototype._storeAttribs = function (elem) {
  170. var attrIndex,
  171. attr,
  172. attrObj;
  173. for (attrIndex = 0; attrIndex < this.attrList.length; attrIndex++) {
  174. attr = this.attrList[attrIndex];
  175. if (elem.attr(attr)) {
  176. // Grab the existing attribute store or create a new object
  177. attrObj = elem.data('lang-attr') || {};
  178. // Add the attribute and value to the store
  179. attrObj[attr] = elem.attr(attr);
  180. // Save the attribute data to the store
  181. elem.data('lang-attr', attrObj);
  182. }
  183. }
  184. };
  185. /**
  186. * Reads the existing content from the element and stores it for
  187. * later use in translation.
  188. * @param elem
  189. * @private
  190. */
  191. Lang.prototype._storeContent = function (elem) {
  192. // Check if the element is an input element
  193. if (elem.is('input')) {
  194. switch (elem.attr('type')) {
  195. case 'button':
  196. case 'submit':
  197. case 'reset':
  198. elem.data('lang-val', elem.val());
  199. break;
  200. }
  201. } else {
  202. // Get the text nodes immediately inside this element
  203. var nodes = this._getTextNodes(elem);
  204. if (nodes) {
  205. elem.data('lang-text', nodes);
  206. }
  207. }
  208. };
  209. /**
  210. * Retrieves the text nodes from an element and returns them.
  211. * @param elem
  212. * @returns {Array|*}
  213. * @private
  214. */
  215. Lang.prototype._getTextNodes = function (elem) {
  216. var nodes = elem.contents(),
  217. nodeArr;
  218. nodeArr = nodes.filter(function () {
  219. this.langDefaultText = this.data;
  220. return this.nodeType === 3;
  221. });
  222. return nodeArr;
  223. };
  224. /**
  225. * Sets text nodes of an element translated based on the passed language.
  226. * @param elem
  227. * @param nodes
  228. * @param lang
  229. * @private
  230. */
  231. Lang.prototype._setTextNodes = function (elem, nodes, lang) {
  232. var index,
  233. textNode,
  234. defaultText,
  235. translation,
  236. langNotDefault = lang !== this.defaultLang;
  237. for (index = 0; index < nodes.length; index++) {
  238. textNode = nodes[index];
  239. if (langNotDefault) {
  240. defaultText = $.trim(textNode.langDefaultText);
  241. if (defaultText) {
  242. // Translate the langDefaultText
  243. translation = this.translate(defaultText, lang);
  244. if (translation) {
  245. try {
  246. // Replace the text with the translated version
  247. textNode.data = textNode.data.split($.trim(textNode.data)).join(translation);
  248. } catch (e) {
  249. }
  250. } else {
  251. console.log('Translation for "' + defaultText + '" not found!');
  252. }
  253. }
  254. } else {
  255. // Replace with original text
  256. try {
  257. textNode.data = textNode.langDefaultText;
  258. } catch (e) {
  259. }
  260. }
  261. }
  262. };
  263. /**
  264. * Translates and sets the attributes of an element to the passed language.
  265. * @param elem
  266. * @param lang
  267. * @private
  268. */
  269. Lang.prototype._translateAttribs = function (elem, lang) {
  270. var attr,
  271. attrObj = elem.data('lang-attr') || {},
  272. translation;
  273. for (attr in attrObj) {
  274. if (attrObj.hasOwnProperty(attr)) {
  275. // Check the element still has the attribute
  276. if (elem.attr(attr)) {
  277. if (lang !== this.defaultLang) {
  278. // Get the translated value
  279. translation = this.translate(attrObj[attr], lang);
  280. // Check we actually HAVE a translation
  281. if (translation) {
  282. // Change the attribute to the translated value
  283. elem.attr(attr, translation);
  284. }
  285. } else {
  286. // Set default language value
  287. elem.attr(attr, attrObj[attr]);
  288. }
  289. }
  290. }
  291. }
  292. };
  293. /**
  294. * Translates and sets the contents of an element to the passed language.
  295. * @param elem
  296. * @param lang
  297. * @private
  298. */
  299. Lang.prototype._translateContent = function (elem, lang) {
  300. var langNotDefault = lang !== this.defaultLang,
  301. translation,
  302. nodes;
  303. // Check if the element is an input element
  304. if (elem.is('input')) {
  305. switch (elem.attr('type')) {
  306. case 'button':
  307. case 'submit':
  308. case 'reset':
  309. if (langNotDefault) {
  310. // Get the translated value
  311. translation = this.translate(elem.data('lang-val'), lang);
  312. // Check we actually HAVE a translation
  313. if (translation) {
  314. // Set translated value
  315. elem.val(translation);
  316. }
  317. } else {
  318. // Set default language value
  319. elem.val(elem.data('lang-val'));
  320. }
  321. break;
  322. }
  323. } else {
  324. // Set text node translated text
  325. nodes = elem.data('lang-text');
  326. if (nodes) {
  327. this._setTextNodes(elem, nodes, lang);
  328. }
  329. }
  330. };
  331. /**
  332. * Call this to change the current language on the page.
  333. * @param {String} lang The new two-letter language code to change to.
  334. * @param {String=} selector Optional selector to find language-based
  335. * elements for updating.
  336. * @param {Function=} callback Optional callback function that will be
  337. * called once the language change has been successfully processed. This
  338. * is especially useful if you are using dynamic language pack loading
  339. * since you will get a callback once it has been loaded and changed.
  340. * Your callback will be passed three arguments, a boolean to denote if
  341. * there was an error (true if error), the second will be the language
  342. * you passed in the change call (the lang argument) and the third will
  343. * be the selector used in the change update.
  344. */
  345. Lang.prototype.change = function (lang, selector, callback) {
  346. var self = this;
  347. if (lang === this.defaultLang || this.pack[lang] || this._dynamic[lang]) {
  348. // Check if the language pack is currently loaded
  349. if (lang !== this.defaultLang) {
  350. if (!this.pack[lang] && this._dynamic[lang]) {
  351. // The language pack needs loading first
  352. console.log('Loading dynamic language pack: ' + this._dynamic[lang] + '...');
  353. this.loadPack(lang, function (err, loadingLang, fromUrl) {
  354. if (!err) {
  355. // Process the change language request
  356. self.change.call(self, lang, selector, callback);
  357. } else {
  358. // Call the callback with the error
  359. if (callback) { callback('Language pack could not load from: ' + fromUrl, lang, selector); }
  360. }
  361. });
  362. return;
  363. } else if (!this.pack[lang] && !this._dynamic[lang]) {
  364. // Pack not loaded and no dynamic entry
  365. console.log('Could not change language to ' + lang + ' because no language pack for this language exists!');
  366. if (callback) { callback('Language pack not defined for: ' + lang, lang, selector); }
  367. }
  368. }
  369. var fireAfterUpdate = false,
  370. currLang = this.currentLang;
  371. if (this.currentLang != lang) {
  372. this.beforeUpdate(currLang, lang);
  373. fireAfterUpdate = true;
  374. }
  375. this.currentLang = lang;
  376. // Get the page HTML
  377. var arr = selector !== undefined ? $(selector).find('[lang]') : $(':not(html)[lang]'),
  378. arrCount = arr.length,
  379. elem;
  380. while (arrCount--) {
  381. elem = $(arr[arrCount]);
  382. if (elem.attr('lang') !== lang) {
  383. this._translateElement(elem, lang);
  384. }
  385. }
  386. if (fireAfterUpdate) {
  387. this.afterUpdate(currLang, lang);
  388. }
  389. // Check for cookie support
  390. if ($.cookie) {
  391. // Set a cookie to remember this language setting with 1 year expiry
  392. $.cookie('langCookie', lang, {
  393. expires: 365,
  394. path: '/'
  395. });
  396. }
  397. if (callback) { callback(false, lang, selector); }
  398. } else {
  399. console.log('Attempt to change language to "' + lang + '" but no language pack for that language is loaded!');
  400. if (callback) { callback('No language pack defined for: ' + lang, lang, selector); }
  401. }
  402. };
  403. Lang.prototype._translateElement = function (elem, lang) {
  404. // Translate attributes
  405. this._translateAttribs(elem, lang);
  406. // Translate content
  407. if (elem.attr('data-lang-content') != 'false') {
  408. this._translateContent(elem, lang);
  409. }
  410. // Update the element's current language
  411. elem.attr('lang', lang);
  412. };
  413. /**
  414. * Translates text from the default language into the passed language.
  415. * @param {String} text The text to translate.
  416. * @param {String} lang The two-letter language code to translate to.
  417. * @returns {*}
  418. */
  419. Lang.prototype.translate = function (text, lang) {
  420. lang = lang || this.currentLang;
  421. if (this.pack[lang]) {
  422. var translation = '';
  423. if (lang != this.defaultLang) {
  424. // Check for a direct token translation
  425. translation = this.pack[lang].token[text];
  426. if (!translation) {
  427. // No token translation was found, test for regex match
  428. translation = this._regexMatch(text, lang);
  429. }
  430. if (!translation) {
  431. console.log('Translation for "' + text + '" not found in language pack: ' + lang);
  432. }
  433. return translation || text;
  434. } else {
  435. return text;
  436. }
  437. } else {
  438. return text;
  439. }
  440. };
  441. /**
  442. * Checks the regex items for a match against the passed text and
  443. * if a match is made, translates to the given replacement.
  444. * @param {String} text The text to test regex matches against.
  445. * @param {String} lang The two-letter language code to translate to.
  446. * @returns {string}
  447. * @private
  448. */
  449. Lang.prototype._regexMatch = function (text, lang) {
  450. // Loop the regex array and test them against the text
  451. var arr,
  452. arrCount,
  453. arrIndex,
  454. item,
  455. regex,
  456. expressionResult;
  457. arr = this.pack[lang].regex;
  458. if (arr) {
  459. arrCount = arr.length;
  460. for (arrIndex = 0; arrIndex < arrCount; arrIndex++) {
  461. item = arr[arrIndex];
  462. regex = item[0];
  463. // Test regex
  464. expressionResult = regex.exec(text);
  465. if (expressionResult && expressionResult[0]) {
  466. return text.split(expressionResult[0]).join(item[1]);
  467. }
  468. }
  469. }
  470. return '';
  471. };
  472. Lang.prototype.beforeUpdate = function (currentLang, newLang) {
  473. if (this._fireEvents) {
  474. $(this).triggerHandler('beforeUpdate', [currentLang, newLang, this.pack[currentLang], this.pack[newLang]]);
  475. }
  476. };
  477. Lang.prototype.afterUpdate = function (currentLang, newLang) {
  478. if (this._fireEvents) {
  479. $(this).triggerHandler('afterUpdate', [currentLang, newLang, this.pack[currentLang], this.pack[newLang]]);
  480. }
  481. };
  482. Lang.prototype.refresh = function () {
  483. // Process refresh on the page
  484. this._fireEvents = false;
  485. this.change(this.currentLang);
  486. this._fireEvents = true;
  487. };
  488. ////////////////////////////////////////////////////
  489. // Mutation overrides
  490. ////////////////////////////////////////////////////
  491. Lang.prototype._mutation = function (context, method, args) {
  492. var result = this._mutationCopies[method].apply(context, args),
  493. currLang = this.currentLang,
  494. rootElem = $(context);
  495. if (rootElem.attr('lang')) {
  496. // Switch off events for the moment
  497. this._fireEvents = false;
  498. // Check if the root element is currently set to another language from current
  499. //if (rootElem.attr('lang') !== this.currentLang) {
  500. this._translateElement(rootElem, this.defaultLang);
  501. this.change(this.defaultLang, rootElem);
  502. // Calling change above sets the global currentLang but this is supposed to be
  503. // an isolated change so reset the global value back to what it was before
  504. this.currentLang = currLang;
  505. // Record data on the default language from the root element
  506. this._processElement(rootElem);
  507. // Translate the root element
  508. this._translateElement(rootElem, this.currentLang);
  509. //}
  510. }
  511. // Record data on the default language from the root's children
  512. this._start(rootElem);
  513. // Process translation on any child elements of this element
  514. this.change(this.currentLang, rootElem);
  515. // Switch events back on
  516. this._fireEvents = true;
  517. return result;
  518. };
  519. return Lang;
  520. })();