jquery-lang.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  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/irrelon/jquery-lang-js
  21. Changelog: See readme.md
  22. */
  23. (function (factory) {
  24. 'use strict';
  25. if (typeof define === 'function' && define.amd) {
  26. define(['jquery', 'Cookies'], factory);
  27. } else if (typeof exports === 'object') {
  28. module.exports = factory();
  29. } else {
  30. var _OldLang = window.Lang;
  31. var api = window.Lang = factory(jQuery, typeof Cookies !== 'undefined' ? Cookies : undefined);
  32. api.noConflict = function () {
  33. window.Lang = _OldLang;
  34. return api;
  35. };
  36. }
  37. }(function ($, Cookies) {
  38. var Lang = function () {
  39. // Enable firing events
  40. this._fireEvents = true;
  41. // Allow storage of dynamic language pack data
  42. this._dynamic = {};
  43. };
  44. /**
  45. * Initialise the library with the library options.
  46. * @param {Object} options The options to init the library with.
  47. * See the readme.md for the details of the options available.
  48. */
  49. Lang.prototype.init = function (options) {
  50. var self = this,
  51. cookieLang,
  52. defaultLang,
  53. currentLang,
  54. allowCookieOverride;
  55. options = options || {};
  56. options.cookie = options.cookie || {};
  57. defaultLang = options.defaultLang;
  58. currentLang = options.currentLang;
  59. allowCookieOverride = options.allowCookieOverride;
  60. // Set cookie settings
  61. this.cookieName = options.cookie.name || 'langCookie';
  62. this.cookieExpiry = options.cookie.expiry || 365;
  63. this.cookiePath = options.cookie.path || '/';
  64. // Store existing mutation methods so we can auto-run
  65. // translations when new data is added to the page
  66. this._mutationCopies = {
  67. append: $.fn.append,
  68. appendTo: $.fn.appendTo,
  69. prepend: $.fn.prepend,
  70. before: $.fn.before,
  71. after: $.fn.after,
  72. html: $.fn.html
  73. };
  74. // Now override the existing mutation methods with our own
  75. $.fn.append = function () {
  76. return self._mutation(this, 'append', arguments)
  77. };
  78. $.fn.appendTo = function () {
  79. return self._mutation(this, 'appendTo', arguments)
  80. };
  81. $.fn.prepend = function () {
  82. return self._mutation(this, 'prepend', arguments)
  83. };
  84. $.fn.before = function () {
  85. return self._mutation(this, 'before', arguments)
  86. };
  87. $.fn.after = function () {
  88. return self._mutation(this, 'after', arguments)
  89. };
  90. $.fn.html = function () {
  91. return self._mutation(this, 'html', arguments)
  92. };
  93. // Set default and current language to the default one
  94. // to start with
  95. this.defaultLang = defaultLang || 'en';
  96. this.currentLang = defaultLang || 'en';
  97. // Check for cookie support when no current language is specified
  98. if ((allowCookieOverride || !currentLang) && typeof Cookies !== 'undefined') {
  99. // Check for an existing language cookie
  100. cookieLang = Cookies.get(this.cookieName);
  101. if (cookieLang) {
  102. // We have a cookie language, set the current language
  103. currentLang = cookieLang;
  104. }
  105. }
  106. $(function () {
  107. // Setup data on the language items
  108. self._start();
  109. // Check if the current language is not the same as our default
  110. if (currentLang && currentLang !== self.defaultLang) {
  111. // Switch to the current language
  112. self.change(currentLang);
  113. }
  114. });
  115. };
  116. /**
  117. * Object that holds the language packs.
  118. * @type {{}}
  119. */
  120. Lang.prototype.pack = {};
  121. /**
  122. * Array of translatable attributes to check for on elements.
  123. * @type {string[]}
  124. */
  125. Lang.prototype.attrList = [
  126. 'title',
  127. 'alt',
  128. 'placeholder',
  129. 'href'
  130. ];
  131. /**
  132. * Defines a language pack that can be dynamically loaded and the
  133. * path to use when doing so.
  134. * @param {String} lang The language two-letter iso-code.
  135. * @param {String} path The path to the language pack js file.
  136. */
  137. Lang.prototype.dynamic = function (lang, path) {
  138. if (lang !== undefined && path !== undefined) {
  139. this._dynamic[lang] = path;
  140. }
  141. };
  142. /**
  143. * Loads a new language pack for the given language.
  144. * @param {string} lang The language to load the pack for.
  145. * @param {Function=} callback Optional callback when the file has loaded.
  146. */
  147. Lang.prototype.loadPack = function (lang, callback) {
  148. var self = this;
  149. if (lang && self._dynamic[lang]) {
  150. $.ajax({
  151. dataType: "json",
  152. url: self._dynamic[lang],
  153. success: function (data) {
  154. self.pack[lang] = data;
  155. // Process the regex list
  156. if (self.pack[lang].regex) {
  157. var packRegex = self.pack[lang].regex,
  158. regex,
  159. i;
  160. for (i = 0; i < packRegex.length; i++) {
  161. regex = packRegex[i];
  162. if (regex.length === 2) {
  163. // String, value
  164. regex[0] = new RegExp(regex[0]);
  165. } else if (regex.length === 3) {
  166. // String, modifiers, value
  167. regex[0] = new RegExp(regex[0], regex[1]);
  168. // Remove modifier
  169. regex.splice(1, 1);
  170. }
  171. }
  172. }
  173. //console.log('Loaded language pack: ' + self._dynamic[lang]);
  174. if (callback) {
  175. callback(false, lang, self._dynamic[lang]);
  176. }
  177. },
  178. error: function () {
  179. if (callback) {
  180. callback(true, lang, self._dynamic[lang]);
  181. }
  182. throw('Organizr Translation Function: Error loading language pack ' + self._dynamic[lang]);
  183. }
  184. });
  185. } else {
  186. throw('Cannot load language pack, no file path specified!');
  187. }
  188. };
  189. /**
  190. * Scans the DOM for elements with [lang] selector and saves translate data
  191. * for them for later use.
  192. * @private
  193. */
  194. Lang.prototype._start = function (selector) {
  195. // Get the page HTML
  196. var arr = selector !== undefined ? $(selector).find('[lang]') : $(':not(html)[lang]'),
  197. arrCount = arr.length,
  198. elem;
  199. while (arrCount--) {
  200. elem = $(arr[arrCount]);
  201. this._processElement(elem);
  202. }
  203. };
  204. Lang.prototype._processElement = function (elem) {
  205. // Only store data if the element is set to our default language
  206. if (elem.attr('lang') === this.defaultLang) {
  207. // Store translatable attributes
  208. this._storeAttribs(elem);
  209. // Store translatable content
  210. this._storeContent(elem);
  211. }
  212. };
  213. /**
  214. * Stores the translatable attribute values in their default language.
  215. * @param {object} elem The jQuery selected element.
  216. * @private
  217. */
  218. Lang.prototype._storeAttribs = function (elem) {
  219. var attrIndex,
  220. attr,
  221. attrObj;
  222. for (attrIndex = 0; attrIndex < this.attrList.length; attrIndex++) {
  223. attr = this.attrList[attrIndex];
  224. if (elem.attr(attr)) {
  225. // Grab the existing attribute store or create a new object
  226. attrObj = elem.data('lang-attr') || {};
  227. // Add the attribute and value to the store
  228. attrObj[attr] = elem.attr(attr);
  229. // Save the attribute data to the store
  230. elem.data('lang-attr', attrObj);
  231. }
  232. }
  233. };
  234. /**
  235. * Reads the existing content from the element and stores it for
  236. * later use in translation.
  237. * @param elem
  238. * @private
  239. */
  240. Lang.prototype._storeContent = function (elem) {
  241. // Check if the element is an input element
  242. if (elem.is('input')) {
  243. switch (elem.attr('type')) {
  244. case 'button':
  245. case 'submit':
  246. case 'hidden':
  247. case 'reset':
  248. elem.data('lang-val', elem.val());
  249. break;
  250. }
  251. } else if (elem.is('img')) {
  252. elem.data('lang-src', elem.attr('src'));
  253. } else {
  254. // Get the text nodes immediately inside this element
  255. var nodes = this._getTextNodes(elem);
  256. if (nodes) {
  257. elem.data('lang-text', nodes);
  258. }
  259. }
  260. };
  261. /**
  262. * Retrieves the text nodes from an element and returns them in array wrap into
  263. * object with two properties:
  264. * - node - which corresponds to text node,
  265. * - langDefaultText - which remember current data of text node
  266. * @param elem
  267. * @returns {Array|*}
  268. * @private
  269. */
  270. Lang.prototype._getTextNodes = function (elem) {
  271. var nodes = elem.contents(), nodeObjArray = [], nodeObj = {},
  272. nodeArr, that = this, map = Array.prototype.map;
  273. $.each(nodes, function (index, node) {
  274. if (node.nodeType !== 3) {
  275. return;
  276. }
  277. nodeObj = {
  278. node: node,
  279. langDefaultText: node.data
  280. };
  281. nodeObjArray.push(nodeObj);
  282. });
  283. // If element has only one text node and data-lang-token is defined
  284. // set langContentKey property to use as a token
  285. if(nodes.length == 1){
  286. nodeObjArray[0].langToken = elem.data('langToken');
  287. }
  288. return nodeObjArray;
  289. };
  290. /**
  291. * Sets text nodes of an element translated based on the passed language.
  292. * @param elem
  293. * @param {Array|*} nodes array of objecs with text node and defaultText returned from _getTextNodes
  294. * @param lang
  295. * @private
  296. */
  297. Lang.prototype._setTextNodes = function (elem, nodes, lang) {
  298. var index,
  299. textNode,
  300. defaultText,
  301. translation,
  302. regex,
  303. langNotDefault = lang !== this.defaultLang;
  304. for (index = 0; index < nodes.length; index++) {
  305. textNode = nodes[index];
  306. if (langNotDefault) {
  307. // If langToken is set, use it as a token
  308. defaultText = textNode.langToken || $.trim(textNode.langDefaultText);
  309. if (defaultText) {
  310. // Translate the langDefaultText
  311. translation = this.translate(defaultText, lang);
  312. // if the text containing HTML tag, processing it
  313. regex = /<[^>]+>/g;
  314. if(regex.test(translation)) {
  315. elem.context.innerHTML = translation;
  316. }else if (translation) {
  317. try {
  318. // Replace the text with the translated version
  319. textNode.node.data = textNode.node.data.split($.trim(textNode.node.data)).join(translation);
  320. } catch (e) {
  321. }
  322. } else {
  323. if (console && console.log) {
  324. console.warn('Organizr Function: Translation for "' + defaultText + '" not found!');
  325. }
  326. }
  327. }
  328. } else {
  329. // Replace with original text
  330. try {
  331. textNode.node.data = textNode.langDefaultText;
  332. } catch (e) {
  333. }
  334. }
  335. }
  336. };
  337. /**
  338. * Translates and sets the attributes of an element to the passed language.
  339. * @param elem
  340. * @param lang
  341. * @private
  342. */
  343. Lang.prototype._translateAttribs = function (elem, lang) {
  344. var attr,
  345. attrObj = elem.data('lang-attr') || {},
  346. translation;
  347. for (attr in attrObj) {
  348. if (attrObj.hasOwnProperty(attr)) {
  349. // Check the element still has the attribute
  350. if (elem.attr(attr)) {
  351. if (lang !== this.defaultLang) {
  352. // Get the translated value
  353. translation = this.translate(attrObj[attr], lang);
  354. // Check we actually HAVE a translation
  355. if (translation) {
  356. // Change the attribute to the translated value
  357. elem.attr(attr, translation);
  358. }
  359. } else {
  360. // Set default language value
  361. elem.attr(attr, attrObj[attr]);
  362. }
  363. }
  364. }
  365. }
  366. };
  367. /**
  368. * Translates and sets the contents of an element to the passed language.
  369. * @param elem
  370. * @param lang
  371. * @private
  372. */
  373. Lang.prototype._translateContent = function (elem, lang) {
  374. var langNotDefault = lang !== this.defaultLang,
  375. translation,
  376. nodes;
  377. // Check if the element is an input element
  378. if (elem.is('input')) {
  379. switch (elem.attr('type')) {
  380. case 'button':
  381. case 'submit':
  382. case 'hidden':
  383. case 'reset':
  384. if (langNotDefault) {
  385. // Get the translated value
  386. translation = this.translate(elem.data('lang-val'), lang);
  387. // Check we actually HAVE a translation
  388. if (translation) {
  389. // Set translated value
  390. elem.val(translation);
  391. }
  392. } else {
  393. // Set default language value
  394. elem.val(elem.data('lang-val'));
  395. }
  396. break;
  397. }
  398. } else if (elem.is('img')) {
  399. if (langNotDefault) {
  400. // Get the translated value
  401. translation = this.translate(elem.data('lang-src'), lang);
  402. // Check we actually HAVE a translation
  403. if (translation) {
  404. // Set translated value
  405. elem.attr('src', translation);
  406. }
  407. } else {
  408. // Set default language value
  409. elem.attr('src', elem.data('lang-src'));
  410. }
  411. } else {
  412. // Set text node translated text
  413. nodes = elem.data('lang-text');
  414. if (nodes) {
  415. this._setTextNodes(elem, nodes, lang);
  416. }
  417. }
  418. };
  419. /**
  420. * Call this to change the current language on the page.
  421. * @param {String} lang The new two-letter language code to change to.
  422. * @param {String=} selector Optional selector to find language-based
  423. * elements for updating.
  424. * @param {Function=} callback Optional callback function that will be
  425. * called once the language change has been successfully processed. This
  426. * is especially useful if you are using dynamic language pack loading
  427. * since you will get a callback once it has been loaded and changed.
  428. * Your callback will be passed three arguments, a boolean to denote if
  429. * there was an error (true if error), the second will be the language
  430. * you passed in the change call (the lang argument) and the third will
  431. * be the selector used in the change update.
  432. */
  433. Lang.prototype.change = function (lang, selector, callback) {
  434. var self = this;
  435. if (lang === this.defaultLang || this.pack[lang] || this._dynamic[lang]) {
  436. // Check if the language pack is currently loaded
  437. if (lang !== this.defaultLang) {
  438. if (!this.pack[lang] && this._dynamic[lang]) {
  439. // The language pack needs loading first
  440. //console.log('Loading dynamic language pack: ' + this._dynamic[lang] + '...');
  441. this.loadPack(lang, function (err, loadingLang, fromUrl) {
  442. if (!err) {
  443. // Process the change language request
  444. self.change.call(self, lang, selector, callback);
  445. } else {
  446. // Call the callback with the error
  447. if (callback) {
  448. callback('Language pack could not load from: ' + fromUrl, lang, selector);
  449. }
  450. }
  451. });
  452. return;
  453. } else if (!this.pack[lang] && !this._dynamic[lang]) {
  454. // Pack not loaded and no dynamic entry
  455. if (callback) {
  456. callback('Language pack not defined for: ' + lang, lang, selector);
  457. }
  458. throw('Could not change language to ' + lang + ' because no language pack for this language exists!');
  459. }
  460. }
  461. var fireAfterUpdate = false,
  462. currLang = this.currentLang;
  463. if (this.currentLang != lang) {
  464. this.beforeUpdate(currLang, lang);
  465. fireAfterUpdate = true;
  466. }
  467. this.currentLang = lang;
  468. // Get the page HTML
  469. var arr = selector !== undefined ? $(selector).find('[lang]') : $(':not(html)[lang]'),
  470. arrCount = arr.length,
  471. elem;
  472. while (arrCount--) {
  473. elem = $(arr[arrCount]);
  474. if (elem.attr('lang') !== lang) {
  475. this._translateElement(elem, lang);
  476. }
  477. }
  478. if (fireAfterUpdate) {
  479. this.afterUpdate(currLang, lang);
  480. }
  481. // Check for cookie support
  482. if (typeof Cookies !== "undefined") {
  483. // Set a cookie to remember this language setting with 1 year expiry
  484. Cookies.set(self.cookieName, lang, {
  485. expires: self.cookieExpiry,
  486. path: self.cookiePath
  487. });
  488. }
  489. if (callback) {
  490. callback(false, lang, selector);
  491. }
  492. } else {
  493. if (callback) {
  494. callback('No language pack defined for: ' + lang, lang, selector);
  495. }
  496. throw('Attempt to change language to "' + lang + '" but no language pack for that language is loaded!');
  497. }
  498. };
  499. Lang.prototype._translateElement = function (elem, lang) {
  500. // Translate attributes
  501. this._translateAttribs(elem, lang);
  502. // Translate content
  503. if (elem.attr('data-lang-content') != 'false') {
  504. this._translateContent(elem, lang);
  505. }
  506. // Update the element's current language
  507. elem.attr('lang', lang);
  508. };
  509. /**
  510. * Translates text from the default language into the passed language.
  511. * @param {String} text The text to translate.
  512. * @param {String} lang The two-letter language code to translate to.
  513. * @returns {*}
  514. */
  515. Lang.prototype.translate = function (text, lang) {
  516. lang = lang || this.currentLang;
  517. if (this.pack[lang]) {
  518. var translation = '';
  519. if (lang != this.defaultLang) {
  520. // Check for a direct token translation
  521. translation = this.pack[lang].token[text];
  522. if (!translation) {
  523. // No token translation was found, test for regex match
  524. translation = this._regexMatch(text, lang);
  525. }
  526. if (!translation) {
  527. if (console && console.log) {
  528. //console.log('Translation for "' + text + '" not found in language pack: ' + lang + ' Type or copy this string into this console to get a full strings missing output: getLangStrings()');
  529. //console.log('Translation not found in language pack: ' + lang + ' Type this command into this console to get a full strings missing output: getLangStrings()');
  530. langStrings['token'][text] = text;
  531. }
  532. }
  533. return translation || text;
  534. } else {
  535. return text;
  536. }
  537. } else {
  538. return text;
  539. }
  540. };
  541. /**
  542. * Checks the regex items for a match against the passed text and
  543. * if a match is made, translates to the given replacement.
  544. * @param {String} text The text to test regex matches against.
  545. * @param {String} lang The two-letter language code to translate to.
  546. * @returns {string}
  547. * @private
  548. */
  549. Lang.prototype._regexMatch = function (text, lang) {
  550. // Loop the regex array and test them against the text
  551. var arr,
  552. arrCount,
  553. arrIndex,
  554. item,
  555. regex,
  556. expressionResult;
  557. arr = this.pack[lang].regex;
  558. if (arr) {
  559. arrCount = arr.length;
  560. for (arrIndex = 0; arrIndex < arrCount; arrIndex++) {
  561. item = arr[arrIndex];
  562. regex = item[0];
  563. // Test regex
  564. expressionResult = regex.exec(text);
  565. if (expressionResult && expressionResult[0]) {
  566. return text.split(expressionResult[0]).join(item[1]);
  567. }
  568. }
  569. }
  570. return '';
  571. };
  572. Lang.prototype.beforeUpdate = function (currentLang, newLang) {
  573. if (this._fireEvents) {
  574. $(this).triggerHandler('beforeUpdate', [currentLang, newLang, this.pack[currentLang], this.pack[newLang]]);
  575. }
  576. };
  577. Lang.prototype.afterUpdate = function (currentLang, newLang) {
  578. if (this._fireEvents) {
  579. $(this).triggerHandler('afterUpdate', [currentLang, newLang, this.pack[currentLang], this.pack[newLang]]);
  580. }
  581. };
  582. Lang.prototype.refresh = function () {
  583. // Process refresh on the page
  584. this._fireEvents = false;
  585. this.change(this.currentLang);
  586. this._fireEvents = true;
  587. };
  588. ////////////////////////////////////////////////////
  589. // Mutation overrides
  590. ////////////////////////////////////////////////////
  591. Lang.prototype._mutation = function (context, method, args) {
  592. var result = this._mutationCopies[method].apply(context, args),
  593. currLang = this.currentLang,
  594. rootElem = $(context);
  595. if (rootElem.attr('lang')) {
  596. // Switch off events for the moment
  597. this._fireEvents = false;
  598. // Check if the root element is currently set to another language from current
  599. //if (rootElem.attr('lang') !== this.currentLang) {
  600. this._translateElement(rootElem, this.defaultLang);
  601. this.change(this.defaultLang, rootElem);
  602. // Calling change above sets the global currentLang but this is supposed to be
  603. // an isolated change so reset the global value back to what it was before
  604. this.currentLang = currLang;
  605. // Record data on the default language from the root element
  606. this._processElement(rootElem);
  607. // Translate the root element
  608. this._translateElement(rootElem, this.currentLang);
  609. //}
  610. }
  611. // Record data on the default language from the root's children
  612. this._start(rootElem);
  613. // Process translation on any child elements of this element
  614. this.change(this.currentLang, rootElem);
  615. // Switch events back on
  616. this._fireEvents = true;
  617. return result;
  618. };
  619. return Lang;
  620. }));