Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
269 lines
11 KiB
Plaintext
269 lines
11 KiB
Plaintext
class POParser {
|
|
static parse(content) {
|
|
const lines = POParser.splitLines(content);
|
|
const messages = [];
|
|
const meta = {};
|
|
let state = 'entry';
|
|
let entry;
|
|
for(let i = 0; i < lines.length; i++){
|
|
const line = lines[i].trim();
|
|
// An empty line indicates the end of an entry
|
|
if (!line) {
|
|
if (state === 'entry' && entry) {
|
|
messages.push(POParser.finishEntry(entry));
|
|
entry = undefined;
|
|
}
|
|
state = 'entry';
|
|
continue;
|
|
}
|
|
if (state === 'meta') {
|
|
if (line.startsWith(POParser.QUOTE)) {
|
|
const rawMetaLine = POParser.extractQuotedString(line, state);
|
|
const metaLine = POParser.unescape(rawMetaLine);
|
|
const cleaned = metaLine.endsWith('\n') ? metaLine.slice(0, -1) : metaLine;
|
|
const separatorIndex = cleaned.indexOf(POParser.META_SEPARATOR);
|
|
if (separatorIndex > 0) {
|
|
const key = cleaned.substring(0, separatorIndex).trim();
|
|
const value = cleaned.substring(separatorIndex + 1).trim();
|
|
meta[key] = value;
|
|
}
|
|
} else {
|
|
POParser.throwWithLine('Encountered unexpected non-quoted metadata line', line);
|
|
}
|
|
} else {
|
|
// Unsupported comment types
|
|
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.TRANSLATOR)) {
|
|
POParser.throwWithLine('Translator comments (#) are not supported, use inline descriptions instead', line);
|
|
}
|
|
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.PREVIOUS)) {
|
|
POParser.throwWithLine('Previous string key comments (#|) are not supported', line);
|
|
}
|
|
// Flag comments
|
|
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.FLAG)) {
|
|
entry = POParser.ensureEntry(entry);
|
|
const flagsText = line.substring(POParser.COMMENTS.FLAG.length).trim();
|
|
entry.flags = flagsText.split(',').map((flag)=>flag.trim()).filter(Boolean);
|
|
continue;
|
|
}
|
|
// Reference comments
|
|
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.REFERENCE)) {
|
|
var _entry;
|
|
entry = POParser.ensureEntry(entry);
|
|
const parts = line.substring(POParser.COMMENTS.REFERENCE.length).trim().split(POParser.FILE_COLUMN_SEPARATOR);
|
|
const path = parts[0];
|
|
let lineNumber;
|
|
if (parts.length > 1) {
|
|
const parsedLine = parseInt(parts[1]);
|
|
if (!isNaN(parsedLine)) {
|
|
lineNumber = parsedLine;
|
|
}
|
|
}
|
|
(_entry = entry).references ?? (_entry.references = []);
|
|
const reference = {
|
|
path
|
|
};
|
|
if (lineNumber) {
|
|
reference.line = lineNumber;
|
|
}
|
|
entry.references.push(reference);
|
|
continue;
|
|
}
|
|
// Extracted comments
|
|
if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.EXTRACTED)) {
|
|
var _entry1;
|
|
entry = POParser.ensureEntry(entry);
|
|
const comment = line.substring(POParser.COMMENTS.EXTRACTED.length).trim();
|
|
(_entry1 = entry).extractedComments ?? (_entry1.extractedComments = []);
|
|
entry.extractedComments.push(comment);
|
|
continue;
|
|
}
|
|
// Check for unsupported features
|
|
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID_PLURAL)) {
|
|
POParser.throwWithLine('Plural forms (msgid_plural) are not supported, use ICU pluralization instead', line);
|
|
}
|
|
// msgctxt
|
|
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGCTXT)) {
|
|
entry = POParser.ensureEntry(entry);
|
|
entry.msgctxt = POParser.unescape(POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGCTXT.length + 1), state));
|
|
continue;
|
|
}
|
|
// msgid
|
|
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID)) {
|
|
entry = POParser.ensureEntry(entry);
|
|
entry.msgid = POParser.unescape(POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGID.length + 1), state));
|
|
if (POParser.isMetaEntry(entry, messages)) {
|
|
state = 'meta';
|
|
entry = undefined;
|
|
}
|
|
continue;
|
|
}
|
|
// msgstr
|
|
if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGSTR)) {
|
|
entry = POParser.ensureEntry(entry);
|
|
entry.msgstr = POParser.unescape(POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGSTR.length + 1), state));
|
|
if (POParser.isMetaEntry(entry, messages)) {
|
|
state = 'meta';
|
|
entry = undefined;
|
|
}
|
|
continue;
|
|
}
|
|
// Multi-line strings are not supported in entry mode
|
|
if (line.startsWith(POParser.QUOTE)) {
|
|
POParser.throwWithLine('Multi-line strings are not supported, use single-line strings instead', line);
|
|
}
|
|
}
|
|
}
|
|
// Finish any remaining entry
|
|
if (state === 'entry' && entry) {
|
|
messages.push(POParser.finishEntry(entry));
|
|
}
|
|
return {
|
|
meta: Object.keys(meta).length > 0 ? meta : undefined,
|
|
messages: messages.length > 0 ? messages : undefined
|
|
};
|
|
}
|
|
static isMetaEntry(entry, messages) {
|
|
return messages.length === 0 && entry.msgid === '' && entry.msgstr === '';
|
|
}
|
|
static serialize(catalog) {
|
|
const lines = [];
|
|
// Metadata
|
|
if (catalog.meta) {
|
|
lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${POParser.QUOTE}`);
|
|
lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${POParser.QUOTE}`);
|
|
for (const [key, value] of Object.entries(catalog.meta)){
|
|
lines.push(`${POParser.QUOTE}${key}${POParser.META_SEPARATOR} ${POParser.escape(value)}${POParser.NEWLINE}${POParser.QUOTE}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
// Messages
|
|
if (catalog.messages) {
|
|
for (const entry of catalog.messages){
|
|
if (entry.extractedComments && entry.extractedComments.length > 0) {
|
|
for (const comment of entry.extractedComments){
|
|
lines.push(`${POParser.COMMENTS.EXTRACTED} ${comment}`);
|
|
}
|
|
}
|
|
if (entry.references && entry.references.length > 0) {
|
|
for (const ref of entry.references){
|
|
let refString = ref.path;
|
|
if (ref.line) {
|
|
refString += `${POParser.FILE_COLUMN_SEPARATOR}${ref.line}`;
|
|
}
|
|
lines.push(`${POParser.COMMENTS.REFERENCE} ${refString}`);
|
|
}
|
|
}
|
|
if (entry.flags && entry.flags.length > 0) {
|
|
lines.push(`${POParser.COMMENTS.FLAG} ${entry.flags.join(POParser.FLAG_SEPARATOR)}`);
|
|
}
|
|
if (entry.msgctxt) {
|
|
lines.push(`${POParser.KEYWORDS.MSGCTXT} ${POParser.QUOTE}${POParser.escape(entry.msgctxt)}${POParser.QUOTE}`);
|
|
}
|
|
lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${POParser.escape(entry.msgid)}${POParser.QUOTE}`);
|
|
lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${POParser.escape(entry.msgstr)}${POParser.QUOTE}`);
|
|
lines.push('');
|
|
}
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
static lineStartsWithPrefix(line, prefix) {
|
|
return line.startsWith(prefix + ' ');
|
|
}
|
|
static throwWithLine(message, line) {
|
|
throw new Error(`${message}:\n> ${line}`);
|
|
}
|
|
static splitLines(content) {
|
|
// Avoid overhead for Unix newlines only
|
|
if (content.includes('\r')) {
|
|
content = content.replace(/\r\n/g, '\n');
|
|
}
|
|
return content.split('\n');
|
|
}
|
|
static ensureEntry(entry) {
|
|
return entry || {};
|
|
}
|
|
static finishEntry(entry) {
|
|
if (entry.msgid == null || entry.msgstr == null) {
|
|
throw new Error('Incomplete message entry: both msgid and msgstr are required');
|
|
}
|
|
return {
|
|
msgctxt: entry.msgctxt,
|
|
msgid: entry.msgid,
|
|
msgstr: entry.msgstr,
|
|
extractedComments: entry.extractedComments,
|
|
references: entry.references,
|
|
flags: entry.flags
|
|
};
|
|
}
|
|
static extractQuotedString(line, state) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed.startsWith(POParser.QUOTE)) {
|
|
POParser.throwWithLine('Incomplete quoted string', line);
|
|
}
|
|
if (!trimmed.endsWith(POParser.QUOTE)) {
|
|
if (state === 'meta') {
|
|
return trimmed.substring(POParser.QUOTE.length);
|
|
}
|
|
POParser.throwWithLine('Incomplete quoted string', line);
|
|
}
|
|
const endIndex = trimmed.length - POParser.QUOTE.length;
|
|
return trimmed.substring(POParser.QUOTE.length, endIndex);
|
|
}
|
|
static escape(value) {
|
|
let result = '';
|
|
for (const char of value){
|
|
const mapped = POParser.ESCAPE_LOOKUP[char];
|
|
result += mapped != null ? `\\${mapped}` : char;
|
|
}
|
|
return result;
|
|
}
|
|
static unescape(value) {
|
|
let result = '';
|
|
for(let i = 0; i < value.length; i++){
|
|
const char = value[i];
|
|
if (char === '\\' && i + 1 < value.length) {
|
|
const nextChar = value[i + 1];
|
|
const mapped = POParser.UNESCAPE_LOOKUP[nextChar];
|
|
if (mapped != null) {
|
|
result += mapped;
|
|
i++;
|
|
continue;
|
|
}
|
|
}
|
|
result += char;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
POParser.KEYWORDS = {
|
|
MSGID: 'msgid',
|
|
MSGSTR: 'msgstr',
|
|
MSGCTXT: 'msgctxt',
|
|
MSGID_PLURAL: 'msgid_plural'
|
|
};
|
|
POParser.COMMENTS = {
|
|
REFERENCE: '#:',
|
|
EXTRACTED: '#.',
|
|
TRANSLATOR: '#',
|
|
FLAG: '#,',
|
|
PREVIOUS: '#|'
|
|
};
|
|
POParser.QUOTE = '"';
|
|
POParser.NEWLINE = '\\n';
|
|
POParser.FILE_COLUMN_SEPARATOR = ':';
|
|
POParser.META_SEPARATOR = ':';
|
|
POParser.FLAG_SEPARATOR = ', ';
|
|
POParser.ESCAPE_LOOKUP = {
|
|
'\\': '\\',
|
|
'"': '"',
|
|
'\n': 'n',
|
|
'\r': 'r',
|
|
'\t': 't'
|
|
};
|
|
POParser.UNESCAPE_LOOKUP = Object.entries(POParser.ESCAPE_LOOKUP).reduce((acc, [char, code])=>{
|
|
acc[code] = char;
|
|
return acc;
|
|
}, {});
|
|
|
|
export { POParser as default };
|