UNPKG

gettext-parser

Version:

Parse and compile gettext po and mo files to/from json, nothing more, nothing less

290 lines (235 loc) 7.51 kB
import { Buffer } from 'node:buffer'; import encoding from 'encoding'; import { HEADERS, foldLine, compareMsgid, formatCharset, generateHeader } from './shared.js'; import contentType from 'content-type'; /** * Exposes general compiler function. Takes a translation * object as a parameter and returns PO object * * @param {Object} table Translation object * @return {Buffer} Compiled PO object */ export default function (table, options) { const compiler = new Compiler(table, options); return compiler.compile(); }; /** * Creates a PO compiler object. * * @constructor * @param {Object} table Translation table to be compiled */ function Compiler (table = {}, options = {}) { this._table = table; this._options = options; this._table.translations = this._table.translations || {}; let { headers = {} } = this._table; headers = Object.keys(headers).reduce((result, key) => { const lowerKey = key.toLowerCase(); if (HEADERS.has(lowerKey)) { result[HEADERS.get(lowerKey)] = headers[key]; } else { result[key] = headers[key]; } return result; }, {}); this._table.headers = headers; if (!('foldLength' in this._options)) { this._options.foldLength = 76; } if (!('escapeCharacters' in this._options)) { this._options.escapeCharacters = true; } if (!('sort' in this._options)) { this._options.sort = false; } if (!('eol' in this._options)) { this._options.eol = '\n'; } this._translations = []; this._handleCharset(); } /** * Converts a comments object to a comment string. The comment object is * in the form of {translator:'', reference: '', extracted: '', flag: '', previous:''} * * @param {Object} comments A comments object * @return {String} A comment string for the PO file */ Compiler.prototype._drawComments = function (comments) { const lines = []; const types = [{ key: 'translator', prefix: '# ' }, { key: 'reference', prefix: '#: ' }, { key: 'extracted', prefix: '#. ' }, { key: 'flag', prefix: '#, ' }, { key: 'previous', prefix: '#| ' }]; types.forEach(type => { if (!comments[type.key]) { return; } comments[type.key].split(/\r?\n|\r/).forEach(line => { lines.push(`${type.prefix}${line}`); }); }); return lines.join(this._options.eol); }; /** * Builds a PO string for a single translation object * * @param {Object} block Translation object * @param {Object} [override] Properties of this object will override `block` properties * @param {boolean} [obsolete] Block is obsolete and must be commented out * @return {String} Translation string for a single object */ Compiler.prototype._drawBlock = function (block, override = {}, obsolete = false) { const response = []; const msgctxt = override.msgctxt || block.msgctxt; const msgid = override.msgid || block.msgid; const msgidPlural = override.msgid_plural || block.msgid_plural; const msgstr = [].concat(override.msgstr || block.msgstr); let comments = override.comments || block.comments; // add comments if (comments && (comments = this._drawComments(comments))) { response.push(comments); } if (msgctxt) { response.push(this._addPOString('msgctxt', msgctxt, obsolete)); } response.push(this._addPOString('msgid', msgid || '', obsolete)); if (msgidPlural) { response.push(this._addPOString('msgid_plural', msgidPlural, obsolete)); msgstr.forEach((msgstr, i) => { response.push(this._addPOString(`msgstr[${i}]`, msgstr || '', obsolete)); }); } else { response.push(this._addPOString('msgstr', msgstr[0] || '', obsolete)); } return response.join(this._options.eol); }; /** * Escapes and joins a key and a value for the PO string * * @param {String} key Key name * @param {String} value Key value * @param {boolean} [obsolete] PO string is obsolete and must be commented out * @return {String} Joined and escaped key-value pair */ Compiler.prototype._addPOString = function (key = '', value = '', obsolete = false) { key = key.toString(); if (obsolete) { key = '#~ ' + key; } let { foldLength, eol, escapeCharacters } = this._options; // escape newlines and quotes if (escapeCharacters) { value = value.toString() .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\t/g, '\\t') .replace(/\r/g, '\\r'); } value = value.replace(/\n/g, '\\n'); // need to escape new line characters regardless let lines = [value]; if (obsolete) { eol = eol + '#~ '; } if (foldLength > 0) { lines = foldLine(value, foldLength); } else { // split only on new lines if (escapeCharacters) { lines = value.split(/\\n/g); for (let i = 0; i < lines.length - 1; i++) { lines[i] = `${lines[i]}\\n`; } if (lines.length && lines[lines.length - 1] === '') { lines.splice(-1, 1); } } } if (lines.length < 2) { return `${key} "${lines.shift() || ''}"`; } return `${key} ""${eol}"${lines.join(`"${eol}"`)}"`; }; /** * Handles header values, replaces or adds (if needed) a charset property */ Compiler.prototype._handleCharset = function () { const ct = contentType.parse(this._table.headers['Content-Type'] || 'text/plain'); const charset = formatCharset(this._table.charset || ct.parameters.charset || 'utf-8'); // clean up content-type charset independently using fallback if missing if (ct.parameters.charset) { ct.parameters.charset = formatCharset(ct.parameters.charset); } this._table.charset = charset; this._table.headers['Content-Type'] = contentType.format(ct); }; /** * Flatten and sort translations object * * @param {Object} section Object to be prepared (translations or obsolete) * @returns {Array} Prepared array */ Compiler.prototype._prepareSection = function (section) { let response = []; Object.keys(section).forEach(msgctxt => { if (typeof section[msgctxt] !== 'object') { return; } Object.keys(section[msgctxt]).forEach(msgid => { if (typeof section[msgctxt][msgid] !== 'object') { return; } if (msgctxt === '' && msgid === '') { return; } response.push(section[msgctxt][msgid]); }); }); const { sort } = this._options; if (sort !== false) { if (typeof sort === 'function') { response = response.sort(sort); } else { response = response.sort(compareMsgid); } } return response; }; /** * Compiles translation object into a PO object * * @return {Buffer} Compiled PO object */ Compiler.prototype.compile = function () { const headerBlock = (this._table.translations[''] && this._table.translations['']['']) || {}; let response = []; const translations = this._prepareSection(this._table.translations); response = translations.map(r => this._drawBlock(r)); if (typeof this._table.obsolete === 'object') { const obsolete = this._prepareSection(this._table.obsolete); if (obsolete.length) { response = response.concat(obsolete.map(r => this._drawBlock(r, {}, true))); } } const { eol } = this._options; response.unshift(this._drawBlock(headerBlock, { msgstr: generateHeader(this._table.headers) })); if (this._table.charset === 'utf-8' || this._table.charset === 'ascii') { return Buffer.from(response.join(eol + eol) + eol, 'utf-8'); } return encoding.convert(response.join(eol + eol) + eol, this._table.charset); };