intl-t
Version:
A Fully-Typed Node-Based i18n Translation Library
166 lines (165 loc) • 9.01 kB
JavaScript
import { format } from "./format";
import { isEdge } from "../state";
const ev = isEdge ? (v) => v : globalThis.eval;
export function nested(content) {
const matches = content.matchAll(/(?<!`)({|})/g);
let open = 0;
let startIndex;
for (const { index, 0: match } of matches) {
if (match == `{`)
open++ == 0 && (startIndex = index);
else if (--open == 0 ? true : open < 0 && (open = 0))
return { value: content.slice(startIndex, index + 1), index: startIndex, offset: index };
}
return null;
}
export const variableRegex = /{{?(\w+|(?:\(.*?\)|[=+!><\-&|%*/?:#\w]|{\w+}|".*?"|'.*?')+)\s*(?:[,.;]+\s*(\w+)\s*)?(?:[,.;]+(.*))?}}?/s;
export const instructionsRegex = /(?:(?<t1>(?<k1>\w+))|(?<kn1>'|")(?<k2>.*?)(?<!`)\k<kn1>|(?<k3>(?:[=+!><\-/&|%*/?:]*(?:#|\(.*?\)|\w+|{\w+}|(?<kn2>'|").*?(?<!`)\k<kn2>|\[.*?\]|\/.*?\/w*))+))(?:[\s=>]|:(?!:\w))*(?:(?<=[\s:=>])(?<t_1>(?<v1>[\w#+-]+))(?=$|[,\s;])|(?<vn1>'|")(?<v2>.*?)(?<!`)\k<vn1>|(?<v3>{.*(?<!`)(?<vn2>})))|:?:?(?<t2>(?<k4>\w+))(?:[\\/](?<t_2>(?<v4>[\w.,+-]+))|\((?:(?<t_3>(?<v5>\w+))|(?<vn3>'|")(?<v6>.*?)(?<!`)\k<vn3>)\))?/s;
const instructionRegex = /{(.*)}/s;
export function instructionsMatch(content) {
const instructions = [];
let match;
while ((match = content.match(instructionsRegex))) {
const { groups = {}, index = 0 } = match;
for (let k of ["k", "v", "t", "kn", "vn", "tn"])
for (let i = 1; !groups[k] && i <= 6; i++)
groups[k] ??= groups[k + i];
if (groups?.vn == "}") {
const { value, offset } = nested(groups.v);
content = content.slice(match[0].indexOf(groups.v) + index + offset);
groups.v = value.match(instructionRegex)?.[1] || value;
}
else
content = content.slice(index + match[0].length);
instructions.push(match);
}
return instructions;
}
export function injectVariables(content = "", variables = {},
// @ts-ignore
state = this || {}) {
if (!content || !variables)
return content;
const { formatFallback = "", formatOptions } = state;
let match;
const matches = new Set();
while ((match = nested(content)?.value.match(variableRegex))) {
let [target, key = formatFallback, action = "", instruction = action] = match;
if (matches.has(target))
break;
matches.add(target);
if (formatFallback)
key = key.replace(/#+/g, formatFallback);
let v;
if (!key.match(/^\w+$/)) {
const k = key.match(/\w+/)?.[0] ?? formatFallback;
key.matchAll(/{(\w+)}|(\w+)/g).forEach(([i, _, v = _]) => v in variables && (key = key.replace(i, JSON.stringify(variables[v]))));
v = ev(key);
key = k;
}
else
v = variables[key];
let value;
if (v === undefined)
break;
else if (v && typeof v === "string" && !isNaN(v))
v = Number(v);
const options = { ...formatOptions };
const instructions = instructionsMatch(instruction).map(({ groups = {} }) => {
const { k: name, v: val, t, t_ } = groups;
action += "\n" + (t || "") + "\n" + (t_ || "");
options[name] = val;
return { name, val };
});
action += `\n${key}`;
switch (true) {
case typeof v === "function" || /^(react|chunk)s?$/im.test(action):
value = `<${key} ${instruction}/>`;
break;
case Array.isArray(v):
value = format.list(v, options, state);
break;
case /^(time)?(relativ[eoa]|remain(ing)?)s?(time?)?$/im.test(action):
value = format.relative(v, options, state);
break;
case v instanceof Date ||
(/^(time|now|hou?ra?|tiempo|today|date|fecha)s?$/im.test(action) &&
(isNaN(new Date(v).getTime()) ? false : ((v = new Date(v)), true))):
if (action.match(/^(short|sm)$/im))
options.style = "short";
else if (action.match(/^(medium|md)$/im))
options.style = "medium";
else if (action.match(/^(long|lg|verbose)$/im))
options.style = "long";
else if (action.match(/^(full|xl)$/im))
options.style = "full";
if (/^verbose$/im.test(action))
(options.timeStyle ??= options.style), (options.dateStyle ??= options.style);
else if (!options.timeStyle && !/^(date|fecha|today)s?$/im.test(action) && /^(time|now|tiempo|hou?ra?)s?$/im.test(action))
options.timeStyle = options.style || "short";
else
options.dateStyle ??= options.style;
value = format.date(v, options, state);
break;
default:
for (let { name, val } of instructions) {
try {
if (v == name.match(/^=?(?<k>'|")(.*)(?<!`)\k<k>$/)?.[1] ||
(/[^\w\s]/.test(name)
? ((name = name.replace(/(?<![=<>&|!])([=&|])(?![=&|])/g, "$1$1")),
/\/.+\/\w*$/.test(name) && (name += ".test(String(#))"),
(name = name.replace(/(?<=[|&])(?=[=<>])/g, "#")),
/^[^\w#"'/({`[\]]/.test(name) ? (name = `#${name}`) : null,
(name = name.replaceAll("#", JSON.stringify(v))),
name.matchAll(/{(\w+)}/g).forEach(([i, v]) => (name = name.replace(i, JSON.stringify(variables[v])))),
ev(name))
: name == v) ||
/^(ot(her|r[ao])|alway)s?$/im.test(name) ||
(v ? /^(yea?[hs]?|y[ue]p|true?|s[íi].?|[sytv])$/im.test(name) : /^(no.?|false?|[nf])$/im.test(name)) ||
(typeof v === "number"
? (v == 0 && /^([zc]ero|draw|tie|tabl[ea])s?$/i.test(name)) ||
(action.includes("ordinal") &&
String(v).match(/^(one|uno)s?$/i.test(name)
? /(?<!1)1$/
: /^(two|do[zs]e?n?)s?$/i.test(name)
? /2$/
: /^(thre+|tre)s?$/i.test(name)
? /3$/
: null)) ||
(v == 1 && /^(one|[ui]nit|uno)s?$/i.test(name)) ||
(v == 2 && /^(two|pai?r|du(ale?|o)|do[sz]e?n?)s?$/i.test(name)) ||
(v == 3 && /^(thre+|tre|trio?)s?$/i.test(name)) ||
(v > 4 && v < Infinity && /^(many|a? *lot|much[ao]?)s?$/i.test(name)) ||
(v > 1 && v < 4 && /^(few|a? *little|poc[ao]|paucal)s?$/i.test(name)) ||
(v < 5 && /^(paucal|vario|alguno|cuanto)s?$/i.test(name)) ||
(v > 0 && /^(surplu|debit|profit|worth)s?$/i.test(name)) ||
(v < 0 && /^(deficit|lose?|de(bt|uda?)|credito?)s?$/i.test(name))
: false)) {
value = val;
break;
}
}
catch { }
}
if (typeof v === "number") {
if (options.offset)
v -= options.offset;
if (options.scale)
v *= options.scale;
if (action.match(/\bint/i))
v = Math.floor(v);
if ((/^(currenc[yi]e?|cashe?|dinero|bill|money|price|coin|monto)s?$/im.test(action) &&
((options.style = "currency"), (options.currency ??= "USD"))) ||
(/^(p[eo]rcent.*|rati?[eo]|taxe?)s?$/im.test(action) && (options.style = "percent")) ||
(/^(n.m(ero|ber)|digit|decimal|c.(pher|fra)|quantit[iy]e?|cantidad)s?$/im.test(action) && (options.style = "decimal")))
v = format.number(v, options, state);
}
value?.includes("{") && (value = injectVariables(value, variables, { ...state, formatOptions: options, formatFallback: key }));
break;
}
value = value?.replace(/(?<!`)#/, String(v)) ?? v;
content = content.replaceAll(target, String(value));
}
return content.replace(/`(.)/, "$1");
}
export { injectVariables as inject };