pocompiler.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. 'use strict';
  2. var Buffer = require('safe-buffer').Buffer;
  3. var encoding = require('encoding');
  4. var sharedFuncs = require('./shared');
  5. /**
  6. * Comparator function for comparing msgid
  7. * @param {Object} object with msgid prev
  8. * @param {Object} object with msgid next
  9. * @returns {number} comparator index
  10. */
  11. function compare (r1, r2) {
  12. if (r1.msgid > r2.msgid) {
  13. return 1;
  14. }
  15. if (r2.msgid > r1.msgid) {
  16. return -1;
  17. }
  18. return 0;
  19. }
  20. /**
  21. * Exposes general compiler function. Takes a translation
  22. * object as a parameter and returns PO object
  23. *
  24. * @param {Object} table Translation object
  25. * @return {Buffer} Compiled PO object
  26. */
  27. module.exports = function (table, options) {
  28. var compiler = new Compiler(table, options);
  29. return compiler.compile();
  30. };
  31. /**
  32. * Creates a PO compiler object.
  33. *
  34. * @constructor
  35. * @param {Object} table Translation table to be compiled
  36. */
  37. function Compiler (table, options) {
  38. this._table = table || {};
  39. this._table.headers = this._table.headers || {};
  40. this._table.translations = this._table.translations || {};
  41. this._options = options || {};
  42. if (!('foldLength' in this._options)) {
  43. this._options.foldLength = 76;
  44. }
  45. if (!('sort' in this._options)) {
  46. this._options.sort = false;
  47. }
  48. this._translations = [];
  49. this._handleCharset();
  50. }
  51. /**
  52. * Converts a comments object to a comment string. The comment object is
  53. * in the form of {translator:'', reference: '', extracted: '', flag: '', previous:''}
  54. *
  55. * @param {Object} comments A comments object
  56. * @return {String} A comment string for the PO file
  57. */
  58. Compiler.prototype._drawComments = function (comments) {
  59. var lines = [];
  60. var types = [{
  61. key: 'translator',
  62. prefix: '# '
  63. }, {
  64. key: 'reference',
  65. prefix: '#: '
  66. }, {
  67. key: 'extracted',
  68. prefix: '#. '
  69. }, {
  70. key: 'flag',
  71. prefix: '#, '
  72. }, {
  73. key: 'previous',
  74. prefix: '#| '
  75. }];
  76. types.forEach(function (type) {
  77. if (!comments[type.key]) {
  78. return;
  79. }
  80. comments[type.key].split(/\r?\n|\r/).forEach(function (line) {
  81. lines.push(type.prefix + line);
  82. });
  83. });
  84. return lines.join('\n');
  85. };
  86. /**
  87. * Builds a PO string for a single translation object
  88. *
  89. * @param {Object} block Translation object
  90. * @param {Object} [override] Properties of this object will override `block` properties
  91. * @return {String} Translation string for a single object
  92. */
  93. Compiler.prototype._drawBlock = function (block, override) {
  94. override = override || {};
  95. var response = [];
  96. var comments = override.comments || block.comments;
  97. var msgctxt = override.msgctxt || block.msgctxt;
  98. var msgid = override.msgid || block.msgid;
  99. var msgidPlural = override.msgid_plural || block.msgid_plural;
  100. var msgstr = [].concat(override.msgstr || block.msgstr);
  101. // add comments
  102. if (comments && (comments = this._drawComments(comments))) {
  103. response.push(comments);
  104. }
  105. if (msgctxt) {
  106. response.push(this._addPOString('msgctxt', msgctxt));
  107. }
  108. response.push(this._addPOString('msgid', msgid || ''));
  109. if (msgidPlural) {
  110. response.push(this._addPOString('msgid_plural', msgidPlural));
  111. msgstr.forEach(function (msgstr, i) {
  112. response.push(this._addPOString('msgstr[' + i + ']', msgstr || ''));
  113. }.bind(this));
  114. } else {
  115. response.push(this._addPOString('msgstr', msgstr[0] || ''));
  116. }
  117. return response.join('\n');
  118. };
  119. /**
  120. * Escapes and joins a key and a value for the PO string
  121. *
  122. * @param {String} key Key name
  123. * @param {String} value Key value
  124. * @return {String} Joined and escaped key-value pair
  125. */
  126. Compiler.prototype._addPOString = function (key, value) {
  127. key = (key || '').toString();
  128. // escape newlines and quotes
  129. value = (value || '').toString()
  130. .replace(/\\/g, '\\\\')
  131. .replace(/"/g, '\\"')
  132. .replace(/\t/g, '\\t')
  133. .replace(/\r/g, '\\r')
  134. .replace(/\n/g, '\\n');
  135. var lines = [value];
  136. if (this._options.foldLength > 0) {
  137. lines = sharedFuncs.foldLine(value, this._options.foldLength);
  138. }
  139. if (lines.length < 2) {
  140. return key + ' "' + (lines.shift() || '') + '"';
  141. } else {
  142. return key + ' ""\n"' + lines.join('"\n"') + '"';
  143. }
  144. };
  145. /**
  146. * Handles header values, replaces or adds (if needed) a charset property
  147. */
  148. Compiler.prototype._handleCharset = function () {
  149. var parts = (this._table.headers['content-type'] || 'text/plain').split(';');
  150. var contentType = parts.shift();
  151. var charset = sharedFuncs.formatCharset(this._table.charset);
  152. var params = [];
  153. params = parts.map(function (part) {
  154. var parts = part.split('=');
  155. var key = parts.shift().trim();
  156. var value = parts.join('=');
  157. if (key.toLowerCase() === 'charset') {
  158. if (!charset) {
  159. charset = sharedFuncs.formatCharset(value.trim() || 'utf-8');
  160. }
  161. return 'charset=' + charset;
  162. }
  163. return part;
  164. });
  165. if (!charset) {
  166. charset = this._table.charset || 'utf-8';
  167. params.push('charset=' + charset);
  168. }
  169. this._table.charset = charset;
  170. this._table.headers['content-type'] = contentType + '; ' + params.join('; ');
  171. this._charset = charset;
  172. };
  173. /**
  174. * Compiles translation object into a PO object
  175. *
  176. * @return {Buffer} Compiled PO object
  177. */
  178. Compiler.prototype.compile = function () {
  179. var response = [];
  180. var headerBlock = (this._table.translations[''] && this._table.translations['']['']) || {};
  181. Object.keys(this._table.translations).forEach(function (msgctxt) {
  182. if (typeof this._table.translations[msgctxt] !== 'object') {
  183. return;
  184. }
  185. Object.keys(this._table.translations[msgctxt]).forEach(function (msgid) {
  186. if (typeof this._table.translations[msgctxt][msgid] !== 'object') {
  187. return;
  188. }
  189. if (msgctxt === '' && msgid === '') {
  190. return;
  191. }
  192. response.push(this._table.translations[msgctxt][msgid]);
  193. }.bind(this));
  194. }.bind(this));
  195. if (this._options.sort !== false) {
  196. if (typeof this._options.sort === 'function') {
  197. response = response.sort(this._options.sort);
  198. } else {
  199. response = response.sort(compare);
  200. }
  201. }
  202. response = response.map(function (r) {
  203. return this._drawBlock(r);
  204. }.bind(this));
  205. response.unshift(this._drawBlock(headerBlock, {
  206. msgstr: sharedFuncs.generateHeader(this._table.headers)
  207. }));
  208. if (this._charset === 'utf-8' || this._charset === 'ascii') {
  209. return Buffer.from(response.join('\n\n'), 'utf-8');
  210. } else {
  211. return encoding.convert(response.join('\n\n'), this._charset);
  212. }
  213. };