Files
klz-cables.com/.pnpm-store/v10/files/c5/43f5106c8d4f08c9716133410c565c41c3221f32cf17f864107e314169955eb823c7ebb25fbab78c69034e802b0f4983d30adf347455900b09409289f01af4
Marc Mintel 5397309103
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
fix(products): fix breadcrumbs and product filtering (backport from main)
2026-02-24 16:04:21 +01:00

178 lines
5.3 KiB
Plaintext

import { isArgumentElement, isDateElement, isNumberElement, isPluralElement, isPoundElement, isSelectElement, isTagElement, isTimeElement, TYPE } from "./types.js";
function cloneDeep(obj) {
if (Array.isArray(obj)) {
// @ts-expect-error meh
return obj.map(cloneDeep);
}
if (obj !== null && typeof obj === "object") {
// @ts-expect-error meh
return Object.keys(obj).reduce((cloned, k) => {
// @ts-expect-error meh
cloned[k] = cloneDeep(obj[k]);
return cloned;
}, {});
}
return obj;
}
/**
* Replace pound elements with number elements referencing the given variable.
* This is needed when nesting plurals - the # in the outer plural should become
* an explicit variable reference when nested inside another plural.
* GH #4202
*/
function replacePoundWithArgument(ast, variableName) {
return ast.map((el) => {
if (isPoundElement(el)) {
// Replace # with {variableName, number}
return {
type: TYPE.number,
value: variableName,
style: null,
location: el.location
};
}
if (isPluralElement(el) || isSelectElement(el)) {
// Recursively process options
const newOptions = {};
for (const key of Object.keys(el.options)) {
newOptions[key] = { value: replacePoundWithArgument(el.options[key].value, variableName) };
}
return {
...el,
options: newOptions
};
}
if (isTagElement(el)) {
return {
...el,
children: replacePoundWithArgument(el.children, variableName)
};
}
return el;
});
}
function hoistPluralOrSelectElement(ast, el, positionToInject) {
// pull this out of the ast and move it to the top
const cloned = cloneDeep(el);
const { options } = cloned;
// GH #4202: Check if there are other plural/select elements after this one
const afterElements = ast.slice(positionToInject + 1);
const hasSubsequentPluralOrSelect = afterElements.some(isPluralOrSelectElement);
cloned.options = Object.keys(options).reduce((all, k) => {
let optionValue = options[k].value;
// GH #4202: If there are subsequent plurals/selects and this is a plural,
// replace # with explicit variable reference to avoid ambiguity
if (hasSubsequentPluralOrSelect && isPluralElement(el)) {
optionValue = replacePoundWithArgument(optionValue, el.value);
}
const newValue = hoistSelectors([
...ast.slice(0, positionToInject),
...optionValue,
...afterElements
]);
all[k] = { value: newValue };
return all;
}, {});
return cloned;
}
function isPluralOrSelectElement(el) {
return isPluralElement(el) || isSelectElement(el);
}
function findPluralOrSelectElement(ast) {
return !!ast.find((el) => {
if (isPluralOrSelectElement(el)) {
return true;
}
if (isTagElement(el)) {
return findPluralOrSelectElement(el.children);
}
return false;
});
}
/**
* Hoist all selectors to the beginning of the AST & flatten the
* resulting options. E.g:
* "I have {count, plural, one{a dog} other{many dogs}}"
* becomes "{count, plural, one{I have a dog} other{I have many dogs}}".
* If there are multiple selectors, the order of which one is hoisted 1st
* is non-deterministic.
* The goal is to provide as many full sentences as possible since fragmented
* sentences are not translator-friendly
* @param ast AST
*/
export function hoistSelectors(ast) {
for (let i = 0; i < ast.length; i++) {
const el = ast[i];
if (isPluralOrSelectElement(el)) {
return [hoistPluralOrSelectElement(ast, el, i)];
}
if (isTagElement(el) && findPluralOrSelectElement([el])) {
throw new Error("Cannot hoist plural/select within a tag element. Please put the tag element inside each plural/select option");
}
}
return ast;
}
/**
* Collect all variables in an AST to Record<string, TYPE>
* @param ast AST to collect variables from
* @param vars Record of variable name to variable type
*/
function collectVariables(ast, vars = new Map()) {
ast.forEach((el) => {
if (isArgumentElement(el) || isDateElement(el) || isTimeElement(el) || isNumberElement(el)) {
if (el.value in vars && vars.get(el.value) !== el.type) {
throw new Error(`Variable ${el.value} has conflicting types`);
}
vars.set(el.value, el.type);
}
if (isPluralElement(el) || isSelectElement(el)) {
vars.set(el.value, el.type);
Object.keys(el.options).forEach((k) => {
collectVariables(el.options[k].value, vars);
});
}
if (isTagElement(el)) {
vars.set(el.value, el.type);
collectVariables(el.children, vars);
}
});
}
/**
* Check if 2 ASTs are structurally the same. This primarily means that
* they have the same variables with the same type
* @param a
* @param b
* @returns
*/
export function isStructurallySame(a, b) {
const aVars = new Map();
const bVars = new Map();
collectVariables(a, aVars);
collectVariables(b, bVars);
if (aVars.size !== bVars.size) {
return {
success: false,
error: new Error(`Different number of variables: [${Array.from(aVars.keys()).join(", ")}] vs [${Array.from(bVars.keys()).join(", ")}]`)
};
}
return Array.from(aVars.entries()).reduce((result, [key, type]) => {
if (!result.success) {
return result;
}
const bType = bVars.get(key);
if (bType == null) {
return {
success: false,
error: new Error(`Missing variable ${key} in message`)
};
}
if (bType !== type) {
return {
success: false,
error: new Error(`Variable ${key} has conflicting types: ${TYPE[type]} vs ${TYPE[bType]}`)
};
}
return result;
}, { success: true });
}