gettext-parser
Version:
Parse and compile gettext po and mo files to/from json, nothing more, nothing less
162 lines (138 loc) • 4.51 kB
JavaScript
// see https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
const PLURAL_FORMS = 'Plural-Forms';
export const HEADERS = new Map([
['project-id-version', 'Project-Id-Version'],
['report-msgid-bugs-to', 'Report-Msgid-Bugs-To'],
['pot-creation-date', 'POT-Creation-Date'],
['po-revision-date', 'PO-Revision-Date'],
['last-translator', 'Last-Translator'],
['language-team', 'Language-Team'],
['language', 'Language'],
['content-type', 'Content-Type'],
['content-transfer-encoding', 'Content-Transfer-Encoding'],
['plural-forms', PLURAL_FORMS]
]);
const PLURAL_FORM_HEADER_NPLURALS_REGEX = /nplurals\s*=\s*(?<nplurals>\d+)/;
/**
* Parses a header string into an object of key-value pairs
*
* @param {String} str Header string
* @return {Object} An object of key-value pairs
*/
export function parseHeader (str = '') {
return str.split('\n')
.reduce((headers, line) => {
const parts = line.split(':');
let key = (parts.shift() || '').trim();
if (key) {
const value = parts.join(':').trim();
key = HEADERS.get(key.toLowerCase()) || key;
headers[key] = value;
}
return headers;
}, {});
}
/**
* Attempts to safely parse 'nplurals" value from "Plural-Forms" header
*
* @param {Object} [headers = {}] An object with parsed headers
* @returns {number} Parsed result
*/
export function parseNPluralFromHeadersSafely (headers = {}, fallback = 1) {
const pluralForms = headers[PLURAL_FORMS];
if (!pluralForms) {
return fallback;
}
const {
groups: { nplurals } = { nplurals: '' + fallback }
} = pluralForms.match(PLURAL_FORM_HEADER_NPLURALS_REGEX) || {};
return parseInt(nplurals, 10) || fallback;
}
/**
* Joins a header object of key value pairs into a header string
*
* @param {Object} header Object of key value pairs
* @return {String} Header string
*/
export function generateHeader (header = {}) {
const keys = Object.keys(header)
.filter(key => !!key);
if (!keys.length) {
return '';
}
return keys.map(key =>
`${key}: ${(header[key] || '').trim()}`
)
.join('\n') + '\n';
}
/**
* Normalizes charset name. Converts utf8 to utf-8, WIN1257 to windows-1257 etc.
*
* @param {String} charset Charset name
* @return {String} Normalized charset name
*/
export function formatCharset (charset = 'iso-8859-1', defaultCharset = 'iso-8859-1') {
return charset.toString()
.toLowerCase()
.replace(/^utf[-_]?(\d+)$/, 'utf-$1')
.replace(/^win(?:dows)?[-_]?(\d+)$/, 'windows-$1')
.replace(/^latin[-_]?(\d+)$/, 'iso-8859-$1')
.replace(/^(us[-_]?)?ascii$/, 'ascii')
.replace(/^charset$/, defaultCharset)
.trim();
}
/**
* Folds long lines according to PO format
*
* @param {String} str PO formatted string to be folded
* @param {Number} [maxLen=76] Maximum allowed length for folded lines
* @return {Array} An array of lines
*/
export function foldLine (str, maxLen = 76) {
const lines = [];
const len = str.length;
let curLine = '';
let pos = 0;
let match;
while (pos < len) {
curLine = str.substr(pos, maxLen);
// ensure that the line never ends with a partial escaping
// make longer lines if needed
while (curLine.substr(-1) === '\\' && pos + curLine.length < len) {
curLine += str.charAt(pos + curLine.length);
}
// ensure that if possible, line breaks are done at reasonable places
if ((match = /.*?\\n/.exec(curLine))) {
// use everything before and including the first line break
curLine = match[0];
} else if (pos + curLine.length < len) {
// if we're not at the end
if ((match = /.*\s+/.exec(curLine)) && /[^\s]/.test(match[0])) {
// use everything before and including the last white space character (if anything)
curLine = match[0];
} else if ((match = /.*[\x21-\x2f0-9\x5b-\x60\x7b-\x7e]+/.exec(curLine)) && /[^\x21-\x2f0-9\x5b-\x60\x7b-\x7e]/.test(match[0])) {
// use everything before and including the last "special" character (if anything)
curLine = match[0];
}
}
lines.push(curLine);
pos += curLine.length;
}
return lines;
}
/**
* Comparator function for comparing msgid
*
* @param {Object} object with msgid prev
* @param {Object} object with msgid next
* @returns {number} comparator index
*/
export function compareMsgid ({ msgid: left }, { msgid: right }) {
if (left < right) {
return -1;
}
if (left > right) {
return 1;
}
return 0;
}