@intlayer/core
Version:
Includes core Intlayer functions like translation, dictionary, and utility functions shared across multiple packages.
307 lines (305 loc) • 9.65 kB
JavaScript
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