pomo.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. (function (ctx)
  2. {
  3. var noop = function () {}, instance;
  4. if (!String.prototype.trim) {
  5. String.prototype.trim = function () {
  6. return this.replace(/^\s+|\s+$/g, '');
  7. };
  8. }
  9. var sprintf = (function () {
  10. function get_type(variable) {
  11. return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
  12. }
  13. function str_repeat(input, multiplier) {
  14. for (var output = []; multiplier > 0; output[--multiplier] = input) {
  15. }
  16. return output.join('');
  17. }
  18. var str_format = function () {
  19. if (!str_format.cache.hasOwnProperty(arguments[0])) {
  20. str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
  21. }
  22. return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
  23. };
  24. str_format.format = function (parse_tree, argv) {
  25. var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
  26. for (i = 0; i < tree_length; i++) {
  27. node_type = get_type(parse_tree[i]);
  28. if (node_type === 'string') {
  29. output.push(parse_tree[i]);
  30. } else if (node_type === 'array') {
  31. match = parse_tree[i];
  32. if (match[2]) {
  33. arg = argv[cursor];
  34. for (k = 0; k < match[2].length; k++) {
  35. if (!arg.hasOwnProperty(match[2][k])) {
  36. throw (sprintf('[sprintf] property "%s" does not exist', match[2][k]));
  37. }
  38. arg = arg[match[2][k]];
  39. }
  40. } else if (match[1]) {
  41. arg = argv[match[1]];
  42. } else {
  43. arg = argv[cursor++];
  44. }
  45. if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
  46. throw (sprintf('[sprintf] expecting number but found %s', get_type(arg)));
  47. }
  48. switch (match[8]) {
  49. case 'b':
  50. arg = arg.toString(2);
  51. break;
  52. case 'c':
  53. arg = String.fromCharCode(arg);
  54. break;
  55. case 'd':
  56. arg = arg >> 0;
  57. break;
  58. case 'e':
  59. arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential();
  60. break;
  61. case 'f':
  62. arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg);
  63. break;
  64. case 'o':
  65. arg = arg.toString(8);
  66. break;
  67. case 's':
  68. arg = (( arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg);
  69. break;
  70. case 'u':
  71. arg = Math.abs(arg);
  72. break;
  73. case 'x':
  74. arg = arg.toString(16);
  75. break;
  76. case 'X':
  77. arg = arg.toString(16).toUpperCase();
  78. break;
  79. }
  80. arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+' + arg : arg);
  81. pad_character = match[4] ? match[4] === '0' ? '0' : match[4].charAt(1) : ' ';
  82. pad_length = match[6] - String(arg).length;
  83. pad = match[6] ? str_repeat(pad_character, pad_length) : '';
  84. output.push(match[5] ? arg + pad : pad + arg);
  85. }
  86. }
  87. return output.join('');
  88. };
  89. str_format.cache = {};
  90. str_format.parse = function (fmt) {
  91. var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
  92. while (_fmt) {
  93. if (( match = /^[^\x25]+/.exec(_fmt)) !== null) {
  94. parse_tree.push(match[0]);
  95. } else if (( match = /^\x25{2}/.exec(_fmt)) !== null) {
  96. parse_tree.push('%');
  97. } else if (( match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
  98. if (match[2]) {
  99. arg_names |= 1;
  100. var field_list = [], replacement_field = match[2], field_match = [];
  101. if (( field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
  102. field_list.push(field_match[1]);
  103. while (( replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
  104. if (( field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
  105. field_list.push(field_match[1]);
  106. } else if (( field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
  107. field_list.push(field_match[1]);
  108. } else {
  109. throw ('[sprintf] huh?');
  110. }
  111. }
  112. } else {
  113. throw ('[sprintf] huh?');
  114. }
  115. match[2] = field_list;
  116. } else {
  117. arg_names |= 2;
  118. }
  119. if (arg_names === 3) {
  120. throw ('[sprintf] mixing positional and named placeholders is not (yet) supported');
  121. }
  122. parse_tree.push(match);
  123. } else {
  124. throw ('[sprintf] huh?');
  125. }
  126. _fmt = _fmt.substring(match[0].length);
  127. }
  128. return parse_tree;
  129. };
  130. return str_format;
  131. })();
  132. var vsprintf = function (fmt, argv) {
  133. argv.unshift(fmt);
  134. return sprintf.apply(null, argv);
  135. };
  136. var isBrowser = function () {
  137. return (typeof(window) !== 'undefined' && !!document && !!document.getElementsByTagName);
  138. };
  139. var escapeString = function (val) {
  140. var return_value;
  141. if (val.constructor != String()) {
  142. return_value = val;
  143. }
  144. if (val.replace) {
  145. return_value = val
  146. .replace(/[\"]/g, '\\"')
  147. .replace(/[\\]/g, '\\')
  148. .replace(/[\/]/g, '\/')
  149. .replace(/[\b]/g, '\\b')
  150. .replace(/[\f]/g, '\\f')
  151. .replace(/[\n]/g, '\\n')
  152. .replace(/[\r]/g, '\\r')
  153. .replace(/[\t]/g, '\\t');
  154. }
  155. return return_value;
  156. };
  157. var unescapeString = function (val) {
  158. var return_value;
  159. if (!!val && !!val.replace) {
  160. return_value = val
  161. .replace("\\n", '\n')
  162. .replace("\\r", '\r')
  163. .replace("\\t", '\t')
  164. .replace("\\f", '\f')
  165. .replace("\\b", '\b')
  166. .replace("\\r", '\r');
  167. }
  168. return return_value;
  169. };
  170. var waitUntilReady = function (callback) {
  171. var itv = setInterval(function () {
  172. if (!!instance && !instance.waiting) {
  173. clearInterval(itv);
  174. callback.call(callback);
  175. }
  176. }, 4);
  177. };
  178. var extend = function(protoProps, staticProps) {
  179. var parent = this;
  180. var child, has = Object.prototype.hasOwnProperty;
  181. if (protoProps && has.call(protoProps, 'constructor')) {
  182. child = protoProps.constructor;
  183. }
  184. else {
  185. child = function(){ return parent.apply(this, arguments); };
  186. }
  187. if(staticProps)
  188. for(var static_prop in staticProps){
  189. child[static_prop] = staticProps[static_prop];
  190. }
  191. var Surrogate = function(){ this.constructor = child; };
  192. Surrogate.prototype = parent.prototype;
  193. child.prototype = new Surrogate;
  194. if (protoProps)
  195. for(var proto_prop in protoProps){
  196. child.prototype[proto_prop] = protoProps[proto_prop];
  197. }
  198. child.extend = extend;
  199. child.__super__ = parent.prototype;
  200. return child;
  201. };
  202. entryExists = function (hash, domain, escaped) {
  203. return !!hash && !!hash.contents && !!hash.contents[domain] && !!hash.contents[domain][escaped];
  204. };
  205. var Errors = {};
  206. Errors.Base = extend.call(Error, {
  207. toString: function(){
  208. return (this.name + ': ' + this.message);
  209. }
  210. });
  211. Errors.CustomError = Errors.Base.extend({
  212. constructor: function (message) {
  213. this.name = 'CustomError';
  214. this.message = message;
  215. this.stack = (new Error()).stack;
  216. }
  217. });
  218. Errors.CustomError.extend = extend;
  219. Errors.UnknownAcquisitionModeError = Errors.CustomError.extend({
  220. constructor: function (mode) {
  221. var message;
  222. mode = !mode ? 'None provided' : mode;
  223. message = 'PO files acquisition mode: "' + mode + '" is not valid.';
  224. Errors.CustomError.call(this, message);
  225. this.name = 'UnknownAcquisitionModeError';
  226. }
  227. });
  228. var Parser = {};
  229. Parser.Object = function (msg) {
  230. var me = this, phrase_count = 1;
  231. //singular by default
  232. for (var key in msg) {
  233. this[key] = msg[key];
  234. }
  235. this.setCount = function (amount) {
  236. phrase_count = amount >> 0;
  237. };
  238. this.toString = function () {
  239. var return_value, canGetPluralIndex;
  240. canGetPluralIndex = !this.isPlural && this.plural_forms && this.plural_forms.push && !!Parser.Object.calculatePluralIndex;
  241. if (canGetPluralIndex) {
  242. var idx = Parser.Object.calculatePluralIndex(phrase_count);
  243. return_value = this.plural_forms[idx];
  244. } else {
  245. return_value = this.translation;
  246. }
  247. return return_value;
  248. }
  249. };
  250. Parser.POFiles = {
  251. parse: function (text, translation_domain) {
  252. text = !text? '' : text;
  253. var getTelltale, extractHeaderInfo, isMultilinePlural, multilineExtract;
  254. getTelltale = function (str) {
  255. return str.substring(0, 12).split(' ')[0].trim();
  256. };
  257. isMultilinePlural = function (line) {
  258. return (!!line && line.match(/msgstr\[[0-9]\]/));
  259. };
  260. extractHeaderInfo = function (header) {
  261. var header_lines = header.split("\n");
  262. for (var i = 0, j = header_lines.length; i < j; i++) {
  263. var header_line = header_lines[i];
  264. if (header_line.indexOf('"Plural-Forms: nplurals') === 0) {
  265. var plural_form = header_line.substring(14).slice(0, -1);
  266. Parser.Object.calculatePluralIndex = function (phrase_count) {
  267. eval(unescapeString(plural_form));
  268. if (typeof(plural) === 'undefined') {
  269. plural = 0;
  270. }
  271. return plural;
  272. }
  273. }
  274. }
  275. };
  276. multilineExtract = function (part_lines) {
  277. var buffer = [], possible;
  278. while (possible = part_lines.shift()) {
  279. possible = possible.trim();
  280. if (possible.indexOf('"') === 0) {
  281. buffer.push(possible.substring(1).slice(0, -1));
  282. }
  283. else {
  284. part_lines.unshift(possible);
  285. break;
  286. }
  287. }
  288. return buffer.join("");
  289. };
  290. var parsed = {},
  291. has_header_info,
  292. header_info,
  293. part,
  294. domain = ( typeof (translation_domain) === 'undefined') ? 'messages' : translation_domain,
  295. counter = 0,
  296. text = text.replace(/\r\n|\r/g, "\n"),
  297. parts;
  298. parsed[domain] = {};
  299. parts = text.split(/\n\n/);
  300. header_info = parts.shift();
  301. has_header_info = extractHeaderInfo(header_info);
  302. if (!has_header_info) {
  303. parts.unshift(header_info);
  304. }
  305. else {
  306. Parser.header_info = extractHeaderInfo(has_header_info);
  307. }
  308. while (part = parts.shift()) {
  309. var message = {},
  310. part_lines = !!part ? part.split(/\n/) : [],
  311. line = '',
  312. escaped;
  313. while (line = part_lines.shift()) {
  314. line = line.trim();
  315. var next_line = part_lines.slice(0, -1)[0],
  316. line_telltale = getTelltale(line),
  317. next_line_telltale = next_line ? getTelltale(next_line) : false;
  318. switch (line_telltale) {
  319. case 'msgctxt':
  320. message.context = line.substring(9).slice(0, -1);
  321. break;
  322. case 'msgid_plural':
  323. message.isPlural = true;
  324. message.plural_id = line.substring(14).slice(0, -1);
  325. break;
  326. case 'msgid':
  327. if (line.indexOf('msgid ""') === 0) {
  328. var possible = next_line.trim();
  329. if (possible.indexOf('"') === 0) {//multiline
  330. message.id = multilineExtract(part_lines)
  331. }
  332. }
  333. else {
  334. if (next_line_telltale = 'msgstr') {
  335. message.id = line.substr(7).slice(0, -1);
  336. }
  337. }
  338. continue;
  339. case 'msgstr':
  340. if (line.indexOf('msgstr ""') === 0) {
  341. possible = next_line.trim();
  342. if (possible.indexOf('"') === 0) {
  343. message.translation = multilineExtract(part_lines);
  344. }
  345. }
  346. else {
  347. message.translation = line.substr(8).slice(0, -1);
  348. }
  349. break;
  350. case '#:':
  351. case '#.':
  352. case '#':
  353. continue;
  354. case '#,':
  355. break;
  356. default:
  357. if (isMultilinePlural(line)) {
  358. var cases = [];
  359. while (isMultilinePlural(line)) {
  360. cases.push(line.substring(10).slice(0, -1));
  361. line = part_lines.shift();
  362. }
  363. message.plural_forms = cases;
  364. message.translation = cases[0];
  365. }
  366. continue;
  367. }
  368. }
  369. if (!!message.id && !!message.translation) {
  370. message = new Parser.Object(message);
  371. escaped = escapeString(message.id);
  372. if (!parsed[domain][message.id]) {
  373. parsed[domain][escaped] = [message];
  374. } else {
  375. parsed[domain][escaped].push(message);
  376. }
  377. }
  378. counter++;
  379. }
  380. return parsed;
  381. },
  382. generate: function () {
  383. throw "Feature unimplemented";
  384. }
  385. };
  386. var Providers = {};
  387. Providers.Base = function (domain) {
  388. var me = this, consumer_callback = [], has = Object.prototype.hasOwnProperty;
  389. me.domain = domain;
  390. me.waiting = true;
  391. me.notifyConsumers = function(data){
  392. for(var pos in consumer_callback){
  393. if(has.call(consumer_callback,pos)){
  394. consumer_callback[pos].call(consumer_callback[pos], data);
  395. }
  396. }
  397. me.waiting = true;
  398. }
  399. me.done = function (callback) {
  400. consumer_callback.push(callback);
  401. var itv = setInterval(function () {
  402. if (!me.waiting) {
  403. me.notifyConsumers(me.parsed);
  404. clearInterval(itv);
  405. }
  406. }, 4);
  407. }
  408. };
  409. Providers.Base.prototype = {
  410. toString: function () {
  411. return '[object Adapter]'
  412. },
  413. getContents: function(){}
  414. };
  415. Providers.Base.extend = extend;
  416. Providers.String = Providers.Base.extend({
  417. constructor: function (literal_string, domain) {
  418. Providers.Base.call(this, domain);
  419. this.parsed = null;
  420. this.unparsed = '' + literal_string;
  421. },
  422. getContents: function () {
  423. var me = this, parsed;
  424. parsed = Parser.POFiles.parse(this.unparsed, this.domain);
  425. if(!parsed){
  426. throw new Errors.ParsingError(this.unparsed);
  427. }
  428. me.parsed = parsed;
  429. me.notifyConsumers(me.parsed);
  430. }
  431. });
  432. if (!isBrowser()) {
  433. Providers.File = Providers.Base.extend({
  434. constructor: function (path_to_file, domain) {
  435. Providers.Base.call(this, domain);
  436. this.path = '' + path_to_file;
  437. },
  438. getContents: function () {
  439. var me = this;
  440. instance.waiting = true;
  441. me.waiting = true;
  442. require('fs').readFile(this.path, 'utf-8', function (err, data) {
  443. if (!err) {
  444. me.parsed = Parser.POFiles.parse(data, me.domain);
  445. me.waiting = false;
  446. }
  447. else {
  448. throw new Errors.FileReaderError(err.path);
  449. }
  450. });
  451. }
  452. });
  453. }
  454. else{
  455. console.log('wat');
  456. }
  457. if (isBrowser()) {
  458. Providers.Ajax = Providers.Base.extend({
  459. constructor: function (url, domain) {
  460. Providers.Base.call(this, domain);
  461. this.url = url;
  462. },
  463. doRequest: function (url, success, error) {
  464. var request = new XMLHttpRequest(), noop = function(){};
  465. request.open('GET', url, true);
  466. success = !success ? noop : success;
  467. error = !error ? noop : error;
  468. try {
  469. request.onreadystatechange = function () {
  470. var data = {error: true};
  471. if (this.readyState === 4) {
  472. if (this.status >= 200 && this.status < 400) {
  473. data = this.responseText;
  474. success.call(success, data);
  475. }
  476. else {
  477. error.call(error, data);
  478. }
  479. }
  480. };
  481. request.send();
  482. }
  483. catch (Whatever) {
  484. error.call(error, {error: true});
  485. }
  486. request = null;
  487. },
  488. /**
  489. * Read all <link> elements with a type="text/x-gettext-translation" into the dictionary
  490. * @param domain
  491. * @constructor
  492. */
  493. getContents: function () {
  494. var domain = this.domain, me = this;
  495. me.waiting = true;
  496. this.doRequest(this.url, function (result) {
  497. var adapter = new Providers.String(result, domain);
  498. adapter.done(function(data){
  499. me.parsed = data;
  500. me.notifyConsumers(data);
  501. });
  502. adapter.getContents();
  503. });
  504. }
  505. });
  506. }
  507. if (isBrowser()) {
  508. Providers.Links = Providers.Ajax.extend({
  509. constructor: function(domain){
  510. Providers.Ajax.call(this,domain);
  511. this.domain = domain;
  512. },
  513. getContents: function () {
  514. var links = document.getElementsByTagName('link'), link, Ajax = Providers.Ajax, me = this;
  515. me.waiting = true;
  516. for (var i = 0, j = links.length; i < j; i++) {
  517. link = links[i];
  518. if (
  519. !!link
  520. && !!link.getAttribute('type')
  521. && !!link.getAttribute('href')
  522. && link.getAttribute('type') === 'text/x-gettext-translation'
  523. ) {
  524. me.waiting = true;
  525. this.doRequest(link.href, function (result) {
  526. var adapter = new Providers.String(result, me.domain);
  527. adapter.done(function(data){
  528. me.parsed = data;
  529. me.notifyConsumers(data);
  530. });
  531. adapter.getContents();
  532. });
  533. }
  534. }
  535. }
  536. });
  537. }
  538. var Pomo = function () {
  539. var me = this, ready_callback;
  540. me.storage = {};
  541. me.waiting = true;
  542. me.VERSION = '0.1.3';
  543. me.domain = 'messages';
  544. me.returnStrings = false;
  545. me.unescapeStrings = false;
  546. me.ready = function (callback) {
  547. ready_callback = callback;
  548. var itv = setInterval(function () {
  549. if (!me.waiting) {
  550. clearInterval(itv);
  551. ready_callback.call(ready_callback);
  552. }
  553. }, 2);
  554. };
  555. me.wipe = function(){
  556. me.storage = {};
  557. me.waiting = true;
  558. };
  559. me.load = function (resource, options) {
  560. options = !options ? {} : options;
  561. me.waiting = true;
  562. var mode = !!options.mode ? options.mode : false,
  563. translation_domain = !!options.translation_domain ? options.translation_domain : 'messages',
  564. provider;
  565. switch (mode) {
  566. case 'literal':
  567. provider = new Providers.String(resource, translation_domain);
  568. break;
  569. case 'file':
  570. provider = new Providers.File(resource, translation_domain);
  571. break;
  572. case 'link':
  573. provider = new Providers.Links(translation_domain);
  574. break;
  575. case 'ajax':
  576. provider = new Providers.Ajax(resource, translation_domain);
  577. break;
  578. default:
  579. throw new Errors.UnknownAcquisitionModeError(mode);
  580. break;
  581. }
  582. provider.done(function (data) {
  583. me.storage.contents = data;
  584. me.waiting = false;
  585. });
  586. provider.getContents();
  587. return this;
  588. };
  589. me.getText = function (msg_id, options) {
  590. msg_id = msg_id.split(/\n/).join('');
  591. options = !options ? {} : options;
  592. var variables = !!options.variables ? options.variables : [],
  593. context = !!options.context ? options.context : false,
  594. domain = !!options.domain ? options.domain : 'messages',
  595. count = !!options.count ? options.count : false,
  596. error_callback = !options.error? noop : options.error,
  597. escaped = escapeString(msg_id);
  598. if (!domain && !me.domain) {
  599. domain = me.domain = 'messages';
  600. }
  601. else if (domain && !me.domain) {
  602. me.domain = domain;
  603. }
  604. else {
  605. me.domain = domain;
  606. }
  607. if (entryExists(me.storage, domain, escaped)) {
  608. var entry = me.storage.contents[domain][escaped];
  609. if (!!context) {
  610. for (var i = 0, j = entry.length; i < j; i++) {
  611. if (entry[i].context && entry[i].context === context) {
  612. entry = entry[i];
  613. break;
  614. }
  615. }
  616. }
  617. if (entry.unshift) {
  618. entry = entry[0];
  619. }
  620. if (!!count) {
  621. entry.setCount(count);
  622. }
  623. var translation = entry,
  624. is_bare_string = (entry.constructor === String());
  625. if (me.returnStrings) {
  626. translation = entry.toString();
  627. }
  628. if (!!variables) {
  629. var bare_string = is_bare_string ? translation : translation.translation,
  630. translated = vsprintf(bare_string, variables);
  631. if (!is_bare_string) {
  632. translation.translation = translated;
  633. }
  634. else {
  635. translation = translated;
  636. }
  637. }
  638. if (is_bare_string && me.unescapeStrings) {
  639. translation = unescapeString(translation);
  640. }
  641. else if (me.unescapeStrings && !is_bare_string) {
  642. translation.translation = unescapeString(translation.translation);
  643. }
  644. return translation;
  645. }
  646. else {
  647. if(error_callback === noop){
  648. return escaped;
  649. }
  650. else{
  651. error_callback(error_callback, msg_id, domain, me.storage.contents);
  652. }
  653. }
  654. };
  655. };
  656. instance = new Pomo();
  657. if (typeof (module) !== 'undefined') {
  658. module.exports = instance;
  659. }
  660. else {
  661. window['Pomo'] = instance;
  662. }
  663. })(this);