UNPKG

@intlayer/core

Version:

Includes core Intlayer functions like translation, dictionary, and utility functions shared across multiple packages.

307 lines (305 loc) 9.65 kB
import { deepTransformNode } from "../interpreter/getContent/deepTransform.mjs"; import { enu as enumeration } from "../transpiler/enumeration/enumeration.mjs"; import { gender } from "../transpiler/gender/gender.mjs"; import { insert as insertion } from "../transpiler/insertion/insertion.mjs"; import { NodeType } from "@intlayer/types"; //#region src/messageFormat/ICU.ts const parseICU = (text) => { let index = 0; const parseNodes = () => { const nodes = []; let currentText = ""; while (index < text.length) { const char = text[index]; if (char === "{") { if (currentText) { nodes.push(currentText); currentText = ""; } index++; nodes.push(parseArgument()); } else if (char === "}") break; else if (char === "'") if (index + 1 < text.length && text[index + 1] === "'") { currentText += "'"; index += 2; } else { const nextQuote = text.indexOf("'", index + 1); if (nextQuote !== -1) { currentText += text.substring(index + 1, nextQuote); index = nextQuote + 1; } else { currentText += "'"; index++; } } else { currentText += char; index++; } } if (currentText) nodes.push(currentText); return nodes; }; const parseArgument = () => { let name = ""; while (index < text.length && /[^,}]/.test(text[index])) { name += text[index]; index++; } name = name.trim(); if (index >= text.length) throw new Error("Unclosed argument"); if (text[index] === "}") { index++; return { type: "argument", name }; } if (text[index] === ",") { index++; let type = ""; while (index < text.length && /[^,}]/.test(text[index])) { type += text[index]; index++; } type = type.trim(); if (index >= text.length) throw new Error("Unclosed argument"); if (text[index] === "}") { index++; return { type: "argument", name, format: { type } }; } if (text[index] === ",") { index++; if (type === "plural" || type === "select") { const options = {}; while (index < text.length && text[index] !== "}") { while (index < text.length && /\s/.test(text[index])) index++; let key = ""; while (index < text.length && /[^{\s]/.test(text[index])) { key += text[index]; index++; } while (index < text.length && /\s/.test(text[index])) index++; if (text[index] !== "{") throw new Error("Expected { after option key"); index++; const value = parseNodes(); if (text[index] !== "}") throw new Error("Expected } after option value"); index++; options[key] = value; while (index < text.length && /\s/.test(text[index])) index++; } index++; if (type === "plural") return { type: "plural", name, options }; else if (type === "select") return { type: "select", name, options }; } else { let style = ""; while (index < text.length && text[index] !== "}") { style += text[index]; index++; } if (index >= text.length) throw new Error("Unclosed argument"); style = style.trim(); index++; return { type: "argument", name, format: { type, style } }; } } } throw new Error("Malformed argument"); }; return parseNodes(); }; const icuNodesToIntlayer = (nodes) => { if (nodes.length === 0) return ""; if (nodes.length === 1 && typeof nodes[0] === "string") return nodes[0]; if (nodes.every((node) => typeof node === "string" || node.type === "argument")) { let str = ""; for (const node of nodes) if (typeof node === "string") str += node; else if (typeof node !== "string" && node.type === "argument") if (node.format) str += `{${node.name}, ${node.format.type}${node.format.style ? `, ${node.format.style}` : ""}}`; else str += `{{${node.name}}}`; return insertion(str); } if (nodes.length === 1) { const node = nodes[0]; if (typeof node === "string") return node; if (node.type === "argument") { if (node.format) return insertion(`{${node.name}, ${node.format.type}${node.format.style ? `, ${node.format.style}` : ""}}`); return insertion(`{{${node.name}}}`); } if (node.type === "plural") { const options = {}; for (const [key, val] of Object.entries(node.options)) { let newKey = key; if (key.startsWith("=")) newKey = key.substring(1); else if (key === "one") newKey = "1"; else if (key === "two") newKey = "2"; else if (key === "few") newKey = "<=3"; else if (key === "many") newKey = ">=4"; else if (key === "other") newKey = "fallback"; options[newKey] = icuNodesToIntlayer(val.map((v) => { if (typeof v === "string") return v.replace(/#/g, `{{${node.name}}}`); return v; })); } options.__intlayer_icu_var = node.name; return enumeration(options); } if (node.type === "select") { const options = {}; for (const [key, val] of Object.entries(node.options)) options[key] = icuNodesToIntlayer(val); const optionKeys = Object.keys(options); if ((options.male || options.female) && optionKeys.every((k) => [ "male", "female", "other", "fallback" ].includes(k))) return gender({ fallback: options.other, male: options.male, female: options.female }); options.__intlayer_icu_var = node.name; return enumeration(options); } } return nodes.map((node) => icuNodesToIntlayer([node])); }; const icuToIntlayerPlugin = { canHandle: (node) => typeof node === "string" && (node.includes("{") || node.includes("}")), transform: (node) => { try { return icuNodesToIntlayer(parseICU(node)); } catch { return node; } } }; const intlayerToIcuPlugin = { canHandle: (node) => typeof node === "string" && (node.includes("{") || node.includes("}")) || node && typeof node === "object" && (node.nodeType === NodeType.Insertion || node.nodeType === NodeType.Enumeration || node.nodeType === NodeType.Gender || node.nodeType === "composite") || Array.isArray(node), transform: (node, props, next) => { if (typeof node === "string") return node.replace(/\{\{([^}]+)\}\}/g, "{$1}"); if (node.nodeType === NodeType.Insertion) return node.insertion.replace(/\{\{([^}]+)\}\}/g, "{$1}"); if (node.nodeType === NodeType.Enumeration) { const options = node.enumeration; const transformedOptions = {}; for (const [key, val] of Object.entries(options)) { if (key === "__intlayer_icu_var") continue; const childVal = next(val, props); transformedOptions[key] = typeof childVal === "string" ? childVal : JSON.stringify(childVal); } let varName = options.__intlayer_icu_var || "n"; if (!options.__intlayer_icu_var) { const match = (transformedOptions.fallback || transformedOptions.other || Object.values(transformedOptions)[0]).match(/\{([a-zA-Z0-9_]+)\}(?!,)/); if (match) varName = match[1]; } const keys = Object.keys(transformedOptions); const pluralKeys = [ "1", "2", "<=3", ">=4", "fallback", "other", "zero", "one", "two", "few", "many" ]; const isPlural = keys.every((k) => pluralKeys.includes(k) || /^[<>=]?\d+(\.\d+)?$/.test(k)); const parts = []; if (isPlural) { for (const [key, val] of Object.entries(transformedOptions)) { let icuKey = key; if (key === "fallback") icuKey = "other"; else if (key === "<=3") icuKey = "few"; else if (key === ">=4") icuKey = "many"; else if (/^\d+$/.test(key)) icuKey = `=${key}`; else if ([ "zero", "few", "many" ].includes(key)) icuKey = key; else icuKey = "other"; let strVal = val; strVal = strVal.replace(new RegExp(`\\{${varName}\\}`, "g"), "#"); parts.push(`${icuKey} {${strVal}}`); } return `{${varName}, plural, ${parts.join(" ")}}`; } else { const entries = Object.entries(transformedOptions).sort(([keyA], [keyB]) => { if (keyA === "fallback" || keyA === "other") return 1; if (keyB === "fallback" || keyB === "other") return -1; return 0; }); for (const [key, val] of entries) { let icuKey = key; if (key === "fallback") icuKey = "other"; parts.push(`${icuKey} {${val}}`); } return `{${varName}, select, ${parts.join(" ")}}`; } } if (node.nodeType === NodeType.Gender) { const options = node.gender; const varName = "gender"; const parts = []; const entries = Object.entries(options).sort(([keyA], [keyB]) => { if (keyA === "fallback") return 1; if (keyB === "fallback") return -1; return 0; }); for (const [key, val] of entries) { let icuKey = key; if (key === "fallback") icuKey = "other"; const childVal = next(val, props); const strVal = typeof childVal === "string" ? childVal : JSON.stringify(childVal); parts.push(`${icuKey} {${strVal}}`); } return `{${varName}, select, ${parts.join(" ")}}`; } if (Array.isArray(node) || node.nodeType === "composite" && Array.isArray(node.composite)) return (Array.isArray(node) ? node : node.composite).map((item) => next(item, props)).join(""); return next(node, props); } }; const intlayerToICUFormatter = (message) => { return deepTransformNode(message, { dictionaryKey: "icu", keyPath: [], plugins: [{ id: "icu", ...intlayerToIcuPlugin }] }); }; const icuToIntlayerFormatter = (message) => { return deepTransformNode(message, { dictionaryKey: "icu", keyPath: [], plugins: [{ id: "icu", ...icuToIntlayerPlugin }] }); }; //#endregion export { icuToIntlayerFormatter, intlayerToICUFormatter }; //# sourceMappingURL=ICU.mjs.map