UNPKG

@lingui/format-po-gettext

Version:

Gettext PO format with gettext-style plurals for Lingui Catalogs

294 lines (287 loc) 10.2 kB
'use strict'; const parser = require('@messageformat/parser'); const pluralsCldr = require('plurals-cldr'); const PO = require('pofile'); const gettextPlurals = require('node-gettext/lib/plurals'); const generateMessageId = require('@lingui/message-utils/generateMessageId'); const formatPo = require('@lingui/format-po'); const cardinals = require('cldr-core/supplemental/plurals.json'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const pluralsCldr__default = /*#__PURE__*/_interopDefaultCompat(pluralsCldr); const PO__default = /*#__PURE__*/_interopDefaultCompat(PO); const gettextPlurals__default = /*#__PURE__*/_interopDefaultCompat(gettextPlurals); const cardinals__default = /*#__PURE__*/_interopDefaultCompat(cardinals); function renameKeys(rules) { const result = {}; Object.keys(rules).forEach((k) => { const newKey = k.match(/[^-]+$/)[0]; result[newKey] = rules[k]; }); return result; } function fillRange(value) { const [start, end] = value.split("~"); const decimals = (start.split(".")[1] || "").length; const mult = Math.pow(10, decimals); const startNum = Number(start); const endNum = Number(end); const range = Array(Math.ceil(endNum * mult - startNum * mult + 1)).fill(0).map((v, idx) => (idx + startNum * mult) / mult); const last = range[range.length - 1]; if (endNum !== last) { throw new Error(`Range create error for ${value}: last value is ${last}`); } return range.map((v) => Number(v)); } function createSamples(src) { let result = []; src.replace(/…/, "").trim().replace(/,$/, "").split(",").map(function(val) { return val.trim(); }).forEach((val) => { if (val.indexOf("~") !== -1) { result = result.concat(fillRange(val)); } else { result.push(Number(val)); } }); return result; } function createLocaleTest(rules) { const result = {}; Object.keys(rules).forEach((form) => { const samples = rules[form].split(/@integer|@decimal/).slice(1); result[form] = []; samples.forEach((sample) => { result[form] = result[form].concat(createSamples(sample)); }); }); return result; } function getCldrPluralSamples() { const pluralRules = {}; Object.entries(cardinals__default.supplemental["plurals-type-cardinal"]).forEach( ([loc, ruleset]) => { const rules = renameKeys(ruleset); pluralRules[loc.toLowerCase()] = createLocaleTest(rules); } ); return pluralRules; } const cldrSamples = getCldrPluralSamples(); function stringifyICUCase(icuCase) { return icuCase.tokens.map((token) => { if (token.type === "content") { return token.value; } else if (token.type === "octothorpe") { return "#"; } else if (token.type === "argument") { return "{" + token.arg + "}"; } else { console.warn( `Unexpected token "${token}" while stringifying plural case "${icuCase}". Token will be ignored.` ); return ""; } }).join(""); } const ICU_PLURAL_REGEX = /^{.*, plural, .*}$/; const ICU_SELECT_REGEX = /^{.*, select(Ordinal)?, .*}$/; const LINE_ENDINGS = /\r?\n/g; const DEFAULT_CTX_PREFIX = "js-lingui:"; function serializePlurals(item, message, id, isGeneratedId, options) { const icuMessage = message.message; const ctxPrefix = options.customICUPrefix || DEFAULT_CTX_PREFIX; if (!icuMessage) { return item; } const _simplifiedMessage = icuMessage.replace(LINE_ENDINGS, " "); if (ICU_PLURAL_REGEX.test(_simplifiedMessage)) { try { const messageAst = parser.parse(icuMessage)[0]; if (messageAst.cases.some( (icuCase) => icuCase.tokens.some((token) => token.type === "plural") )) { console.warn( `Nested plurals cannot be expressed with gettext plurals. Message with key "%s" will not be saved correctly.`, id ); } const ctx = new URLSearchParams({ pluralize_on: messageAst.arg }); if (isGeneratedId) { item.msgid = stringifyICUCase(messageAst.cases[0]); item.msgid_plural = stringifyICUCase( messageAst.cases[messageAst.cases.length - 1] ); ctx.set("icu", icuMessage); } else { item.msgid_plural = id + "_plural"; } ctx.sort(); item.extractedComments.push(ctxPrefix + ctx.toString()); if (message.translation?.length > 0) { const ast = parser.parse(message.translation)[0]; if (ast.cases == null) { console.warn( `Found translation without plural cases for key "${id}". This likely means that a translated .po file misses multiple msgstr[] entries for the key. Translation found: "${message.translation}"` ); item.msgstr = [message.translation]; } else { item.msgstr = ast.cases.map(stringifyICUCase); } } } catch (e) { console.error(`Error parsing message ICU for key "${id}":`, e); } } else { if (!options.disableSelectWarning && ICU_SELECT_REGEX.test(_simplifiedMessage)) { console.warn( `ICU 'select' and 'selectOrdinal' formats cannot be expressed natively in gettext format. Item with key "%s" will be included in the catalog as raw ICU message. To disable this warning, include '{ disableSelectWarning: true }' in the config's 'formatOptions'`, id ); } item.msgstr = [message.translation]; } return item; } const getPluralCases = (lang, pluralFormsHeader) => { let gettextPluralsInfo; if (pluralFormsHeader) { gettextPluralsInfo = parsePluralFormsFn(pluralFormsHeader); } const [correctLang] = lang.split(/[-_]/g); if (!gettextPluralsInfo) { gettextPluralsInfo = gettextPlurals__default[correctLang]; } if (!gettextPluralsInfo) { if (lang !== "pseudo") { console.warn( `No plural rules found for language "${lang}". Please add a Plural-Forms header.` ); } return void 0; } const cases = [...Array(pluralsCldr__default.forms(correctLang).length)]; for (const form of pluralsCldr__default.forms(correctLang)) { const samples = cldrSamples[correctLang][form]; const pluralForm = Number( gettextPluralsInfo.pluralsFunc(Number(samples[0])) ); cases[pluralForm] = form; } return cases; }; function parsePluralFormsFn(pluralFormsHeader) { const [npluralsExpr, expr] = pluralFormsHeader.split(";"); try { const nplurals = new Function(npluralsExpr + "; return nplurals;")(); const pluralsFunc = new Function( "n", expr + "; return plural;" ); return { nplurals, pluralsFunc }; } catch (e) { console.warn( `Plural-Forms header has incorrect value: ${pluralFormsHeader}` ); return void 0; } } const convertPluralsToICU = (item, pluralForms, lang, ctxPrefix = DEFAULT_CTX_PREFIX) => { const translationCount = item.msgstr.length; const messageKey = item.msgid; if (translationCount <= 1 && !item.msgid_plural) { return; } if (!item.msgid_plural) { console.warn( `Multiple translations for item with key "%s" but missing 'msgid_plural' in catalog "${lang}". This is not supported and the plural cases will be ignored.`, messageKey ); return; } const contextComment = item.extractedComments.find((comment) => comment.startsWith(ctxPrefix))?.substring(ctxPrefix.length); const ctx = new URLSearchParams(contextComment); if (contextComment != null) { item.extractedComments = item.extractedComments.filter( (comment) => !comment.startsWith(ctxPrefix) ); } const storedICU = ctx.get("icu"); if (storedICU != null) { item.msgid = storedICU; } if (item.msgstr.every((str) => str.length === 0)) { return; } if (pluralForms == null) { console.warn( `Multiple translations for item with key "%s" in language "${lang}", but no plural cases were found. This prohibits the translation of .po plurals into ICU plurals. Pluralization will not work for this key.`, messageKey ); return; } const pluralCount = pluralForms.length; if (translationCount > pluralCount) { console.warn( `More translations provided (${translationCount}) for item with key "%s" in language "${lang}" than there are plural cases available (${pluralCount}). This will result in not all translations getting picked up.`, messageKey ); } const pluralClauses = item.msgstr.map( (str, index) => pluralForms[index] ? pluralForms[index] + " {" + str + "}" : "" ).join(" "); let pluralizeOn = ctx.get("pluralize_on"); if (!pluralizeOn) { console.warn( `Unable to determine plural placeholder name for item with key "%s" in language "${lang}" (should be stored in a comment starting with "#. ${ctxPrefix}"), assuming "count".`, messageKey ); pluralizeOn = "count"; } item.msgstr = ["{" + pluralizeOn + ", plural, " + pluralClauses + "}"]; }; function formatter(options = {}) { options = { origins: true, lineNumbers: true, ...options }; const formatter2 = formatPo.formatter(options); return { catalogExtension: ".po", templateExtension: ".pot", parse(content, ctx) { const po = PO__default.parse(content); const pluralForms = getPluralCases( po.headers.Language, po.headers["Plural-Forms"] ); po.items.forEach((item) => { convertPluralsToICU( item, pluralForms, po.headers.Language, options.customICUPrefix ); }); return formatter2.parse(po.toString(), ctx); }, serialize(catalog, ctx) { const po = PO__default.parse(formatter2.serialize(catalog, ctx)); po.items = po.items.map((item) => { const isGeneratedId = !item.extractedComments.includes( "js-lingui-explicit-id" ); const id = isGeneratedId ? generateMessageId.generateMessageId(item.msgid, item.msgctxt) : item.msgid; const message = catalog[id]; return serializePlurals(item, message, id, isGeneratedId, options); }); return po.toString(); } }; } exports.formatter = formatter;