@lingui/format-po
Version:
Gettext PO formatter for Lingui message catalogs
178 lines (174 loc) • 6.07 kB
JavaScript
import PO from 'pofile';
import { generateMessageId } from '@lingui/message-utils/generateMessageId';
function normalizePlaceholderValue(text) {
return text.replace(/\n/g, " ").replace(/\s{2,}/g, " ");
}
function formatPotCreationDate(date) {
const pad = (n) => String(n).padStart(2, "0");
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hour = pad(date.getHours());
const minute = pad(date.getMinutes());
const offsetMinutes = -date.getTimezoneOffset();
const sign = offsetMinutes >= 0 ? "+" : "-";
const abs = Math.abs(offsetMinutes);
const offsetH = pad(Math.floor(abs / 60));
const offsetM = pad(abs % 60);
return `${year}-${month}-${day} ${hour}:${minute}${sign}${offsetH}${offsetM}`;
}
const splitOrigin = (origin) => {
const [file, line] = origin.split(":");
return [file, line ? Number(line) : null];
};
const splitMultiLineComments = (comments) => {
return comments.flatMap(
(comment) => comment.includes("\n") ? comment.split("\n").map((slice) => slice.trim()).filter(Boolean) : comment
);
};
const joinOrigin = (origin) => origin.join(":");
function isGeneratedId(id, message) {
return id === generateMessageId(message.message, message.context);
}
function getCreateHeaders(language, customHeaderAttributes) {
return {
"POT-Creation-Date": formatPotCreationDate(/* @__PURE__ */ new Date()),
"MIME-Version": "1.0",
"Content-Type": "text/plain; charset=utf-8",
"Content-Transfer-Encoding": "8bit",
"X-Generator": "@lingui/cli",
...language ? { Language: language } : {},
...customHeaderAttributes ?? {}
};
}
const EXPLICIT_ID_FLAG = "js-lingui-explicit-id";
const GENERATED_ID_FLAG = "js-lingui-generated-id";
const serialize = (catalog, options, ctx) => {
return Object.keys(catalog).map((id) => {
const message = catalog[id];
const item = new PO.Item();
item.extractedComments = [
...message.comments?.length ? splitMultiLineComments(message.comments) : []
];
item.flags = (message.extra?.flags || []).reduce((acc, flag) => {
acc[flag] = true;
return acc;
}, {});
const _isGeneratedId = isGeneratedId(id, message);
if (_isGeneratedId) {
item.msgid = message.message;
if (options.explicitIdAsDefault) {
if (!item.extractedComments.includes(GENERATED_ID_FLAG)) {
item.extractedComments.push(GENERATED_ID_FLAG);
}
}
if (options.printLinguiId) {
if (!item.extractedComments.find((c) => c.includes("js-lingui-id"))) {
item.extractedComments.push(`js-lingui-id: ${id}`);
}
}
} else {
if (!options.explicitIdAsDefault) {
if (!item.extractedComments.includes(EXPLICIT_ID_FLAG)) {
item.extractedComments.push(EXPLICIT_ID_FLAG);
}
}
item.msgid = id;
}
if (options.printPlaceholdersInComments !== false && message.placeholders) {
item.extractedComments = item.extractedComments.filter(
(comment) => !comment.startsWith("placeholder ")
);
const limit = typeof options.printPlaceholdersInComments === "object" && options.printPlaceholdersInComments.limit ? options.printPlaceholdersInComments.limit : 3;
if (message.placeholders) {
Object.entries(message.placeholders).forEach(([name, value]) => {
if (/^\d+$/.test(name)) {
value.slice(0, limit).forEach((entry) => {
item.extractedComments.push(
`placeholder {${name}}: ${normalizePlaceholderValue(entry)}`
);
});
}
});
}
}
if (message.context) {
item.msgctxt = message.context;
}
if (!_isGeneratedId && (ctx.locale === ctx.sourceLocale || !ctx.locale)) {
item.msgstr = [message.translation || message.message];
} else {
item.msgstr = [message.translation];
}
item.comments = message.extra?.translatorComments || [];
if (options.origins !== false) {
if (message.origin && options.lineNumbers === false) {
item.references = [...new Set(message.origin.map(([path]) => path))];
} else {
item.references = message.origin ? message.origin.map(joinOrigin) : [];
}
}
item.obsolete = message.obsolete || false;
return item;
});
};
function deserialize(items, options) {
return items.reduce((catalog, item) => {
const comments = item.extractedComments;
const message = {
translation: item.msgstr[0],
comments: comments.filter(
// drop flags from comments
(c) => c !== GENERATED_ID_FLAG && c !== EXPLICIT_ID_FLAG
),
context: item.msgctxt ?? void 0,
obsolete: item.flags.obsolete || item.obsolete,
origin: (item.references || []).map((ref) => splitOrigin(ref)),
extra: {
translatorComments: item.comments || [],
flags: Object.keys(item.flags).map((flag) => flag.trim())
}
};
let id = item.msgid;
if (options.explicitIdAsDefault ? comments.includes(GENERATED_ID_FLAG) : !comments.includes(EXPLICIT_ID_FLAG)) {
id = generateMessageId(item.msgid, item.msgctxt);
message.message = item.msgid;
}
catalog[id] = message;
return catalog;
}, {});
}
function formatter(options = {}) {
options = {
origins: true,
lineNumbers: true,
...options
};
return {
catalogExtension: ".po",
templateExtension: ".pot",
parse(content) {
const po = PO.parse(content);
return deserialize(po.items, options);
},
serialize(catalog, ctx) {
let po;
if (ctx.existing) {
po = PO.parse(ctx.existing);
} else {
po = new PO();
po.headers = getCreateHeaders(
ctx.locale,
options.customHeaderAttributes
);
po.headerOrder = Object.keys(po.headers);
}
po.items = serialize(catalog, options, {
locale: ctx.locale,
sourceLocale: ctx.sourceLocale
});
return po.toString();
}
};
}
export { formatter };