@birhaus/test-utils
Version:
BIRHAUS Testing & Validation Framework - Comprehensive testing utilities for cognitive load, accessibility, and BIRHAUS principle compliance
567 lines (564 loc) • 21.2 kB
JavaScript
;
// src/spanish/SpanishCoverageValidator.ts
var SpanishCoverageValidator = class {
constructor(options = {}) {
// Paraguay-specific financial terminology
this.financialTerms = {
// Currency and amounts
"amount": { es: "monto", alternatives: ["cantidad", "importe"] },
"balance": { es: "saldo", alternatives: ["balance"] },
"income": { es: "ingreso", alternatives: ["entrada"] },
"expense": { es: "gasto", alternatives: ["egreso"] },
"transfer": { es: "transferencia", alternatives: [] },
"payment": { es: "pago", alternatives: [] },
"receipt": { es: "recibo", alternatives: ["comprobante"] },
"invoice": { es: "factura", alternatives: [] },
"tax": { es: "impuesto", alternatives: [] },
"total": { es: "total", alternatives: [] },
"subtotal": { es: "subtotal", alternatives: [] },
// Status terms
"pending": { es: "pendiente", alternatives: ["en espera"] },
"approved": { es: "aprobado", alternatives: ["autorizado"] },
"rejected": { es: "rechazado", alternatives: ["denegado"] },
"completed": { es: "completado", alternatives: ["finalizado"] },
"cancelled": { es: "cancelado", alternatives: ["anulado"] },
// Actions
"save": { es: "guardar", alternatives: [] },
"cancel": { es: "cancelar", alternatives: [] },
"edit": { es: "editar", alternatives: ["modificar"] },
"delete": { es: "eliminar", alternatives: ["borrar"] },
"approve": { es: "aprobar", alternatives: ["autorizar"] },
"reject": { es: "rechazar", alternatives: ["denegar"] },
"export": { es: "exportar", alternatives: [] },
"print": { es: "imprimir", alternatives: [] },
// Church-specific terms
"donation": { es: "donaci\xF3n", alternatives: ["ofrenda"] },
"tithe": { es: "diezmo", alternatives: [] },
"offering": { es: "ofrenda", alternatives: [] },
"church": { es: "iglesia", alternatives: [] },
"pastor": { es: "pastor", alternatives: [] },
"member": { es: "miembro", alternatives: [] },
"treasurer": { es: "tesorero", alternatives: [] },
"administrator": { es: "administrador", alternatives: [] }
};
// Common Spanish patterns for Paraguay
this.spanishPatterns = [
/[ñáéíóúü]/i,
// Spanish characters
/ción\b/i,
// -ción endings
/dad\b/i,
// -dad endings
/mente\b/i,
// -mente adverbs
/\bel\b|\bla\b|\blos\b|\blas\b/i,
// Articles
/\ben\b|\bde\b|\bdel\b|\bal\b/i,
// Prepositions
/\by\b|\bo\b|\bpero\b|\bsi\b/i
// Conjunctions
];
this.options = {
requireSpanishPrimary: true,
requireEnglishFallback: true,
strictFinancialTerms: true,
validateParaguayanSpanish: true,
validateFinancialVocabulary: true,
validateCurrencyFormats: true,
checkBirhausComponents: true,
checkErrorMessages: true,
checkButtonLabels: true,
checkFormLabels: true,
birhausSelector: "[data-birhaus-component]",
interactiveSelector: 'button, a, input, select, textarea, [role="button"]',
contentSelector: 'h1, h2, h3, h4, h5, h6, p, span, div[class*="text"], label',
...options
};
}
/**
* Validate all Spanish coverage requirements
*/
validateAll(container = document.body) {
const violations = [];
if (this.options.checkBirhausComponents) {
violations.push(...this.validateBirhausComponents(container));
}
if (this.options.checkButtonLabels) {
violations.push(...this.validateButtonLabels(container));
}
if (this.options.checkFormLabels) {
violations.push(...this.validateFormLabels(container));
}
if (this.options.checkErrorMessages) {
violations.push(...this.validateErrorMessages(container));
}
if (this.options.validateFinancialVocabulary) {
violations.push(...this.validateFinancialTerminology(container));
}
if (this.options.validateCurrencyFormats) {
violations.push(...this.validateCurrencyFormats(container));
}
return violations;
}
/**
* Validate BIRHAUS components have Spanish-first labels
*/
validateBirhausComponents(container) {
const violations = [];
const birhausComponents = container.querySelectorAll(this.options.birhausSelector);
birhausComponents.forEach((component) => {
const componentType = component.getAttribute("data-birhaus-component");
const hasSpanishLabel = this.checkSpanishLabels(component);
const hasEnglishFallback = this.checkEnglishFallbacks(component);
if (this.options.requireSpanishPrimary && !hasSpanishLabel) {
violations.push({
type: "missing-spanish",
element: component,
severity: "serious",
message: `BIRHAUS ${componentType} component missing Spanish labels`,
messageEs: `Componente BIRHAUS ${componentType} carece de etiquetas en espa\xF1ol`,
suggestedSpanish: this.suggestSpanishLabel(component),
birhausPrinciple: 7,
context: this.getElementContext(component)
});
}
if (this.options.requireEnglishFallback && hasSpanishLabel && !hasEnglishFallback) {
violations.push({
type: "missing-english",
element: component,
severity: "moderate",
message: `BIRHAUS ${componentType} component missing English fallback labels`,
messageEs: `Componente BIRHAUS ${componentType} carece de etiquetas de respaldo en ingl\xE9s`,
suggestedEnglish: this.suggestEnglishLabel(component),
birhausPrinciple: 7,
context: this.getElementContext(component)
});
}
});
return violations;
}
/**
* Validate button labels are in Spanish-first
*/
validateButtonLabels(container) {
const violations = [];
const buttons = container.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]');
buttons.forEach((button) => {
const text = this.getElementText(button);
const hasSpanish = this.isSpanishText(text);
if (!hasSpanish && text.length > 0) {
const suggestedSpanish = this.translateButtonText(text);
violations.push({
type: "missing-spanish",
element: button,
severity: "moderate",
message: `Button text "${text}" should be in Spanish`,
messageEs: `Texto del bot\xF3n "${text}" deber\xEDa estar en espa\xF1ol`,
currentText: text,
suggestedSpanish,
birhausPrinciple: 7,
context: "button"
});
}
});
return violations;
}
/**
* Validate form labels and placeholders
*/
validateFormLabels(container) {
const violations = [];
const formElements = container.querySelectorAll("input, select, textarea");
formElements.forEach((element) => {
const label = this.getAssociatedLabel(element);
if (label) {
const labelText = label.textContent?.trim() || "";
if (!this.isSpanishText(labelText) && labelText.length > 0) {
violations.push({
type: "missing-spanish",
element: label,
severity: "moderate",
message: `Form label "${labelText}" should be in Spanish`,
messageEs: `Etiqueta de formulario "${labelText}" deber\xEDa estar en espa\xF1ol`,
currentText: labelText,
suggestedSpanish: this.translateFormLabel(labelText),
birhausPrinciple: 7,
context: "form"
});
}
}
const placeholder = element.getAttribute("placeholder");
if (placeholder && !this.isSpanishText(placeholder)) {
violations.push({
type: "missing-spanish",
element,
severity: "minor",
message: `Placeholder "${placeholder}" should be in Spanish`,
messageEs: `Marcador de posici\xF3n "${placeholder}" deber\xEDa estar en espa\xF1ol`,
currentText: placeholder,
suggestedSpanish: this.translatePlaceholder(placeholder),
birhausPrinciple: 7,
context: "form"
});
}
});
return violations;
}
/**
* Validate error messages are in Spanish with "Qué pasó + cómo resolver" pattern
*/
validateErrorMessages(container) {
const violations = [];
const errorElements = container.querySelectorAll(
'.error, .alert-error, [role="alert"], [class*="error"], [data-error], [aria-invalid="true"] + *'
);
errorElements.forEach((element) => {
const errorText = element.textContent?.trim() || "";
if (errorText.length > 0) {
const hasSpanish = this.isSpanishText(errorText);
const hasProperPattern = this.hasProperErrorPattern(errorText);
if (!hasSpanish) {
violations.push({
type: "missing-spanish",
element,
severity: "serious",
message: `Error message "${errorText}" should be in Spanish`,
messageEs: `Mensaje de error "${errorText}" deber\xEDa estar en espa\xF1ol`,
currentText: errorText,
suggestedSpanish: this.translateErrorMessage(errorText),
birhausPrinciple: 7,
context: "error"
});
} else if (!hasProperPattern) {
violations.push({
type: "invalid-pattern",
element,
severity: "moderate",
message: `Error message should follow "Qu\xE9 pas\xF3 + c\xF3mo resolver" pattern`,
messageEs: `Mensaje de error deber\xEDa seguir el patr\xF3n "Qu\xE9 pas\xF3 + c\xF3mo resolver"`,
currentText: errorText,
suggestedSpanish: this.improveErrorPattern(errorText),
birhausPrinciple: 7,
context: "error"
});
}
}
});
return violations;
}
/**
* Validate financial terminology is properly translated
*/
validateFinancialTerminology(container) {
const violations = [];
const textElements = container.querySelectorAll(this.options.contentSelector);
textElements.forEach((element) => {
const text = element.textContent?.toLowerCase() || "";
Object.entries(this.financialTerms).forEach(([englishTerm, spanishData]) => {
const regex = new RegExp(`\\b${englishTerm}\\b`, "gi");
if (regex.test(text)) {
violations.push({
type: "financial-term",
element,
severity: this.options.strictFinancialTerms ? "serious" : "moderate",
message: `Financial term "${englishTerm}" should use Spanish "${spanishData.es}"`,
messageEs: `T\xE9rmino financiero "${englishTerm}" deber\xEDa usar espa\xF1ol "${spanishData.es}"`,
currentText: englishTerm,
suggestedSpanish: spanishData.es,
birhausPrinciple: 7,
context: "financial"
});
}
});
});
return violations;
}
/**
* Validate currency formats follow Paraguay conventions
*/
validateCurrencyFormats(container) {
const violations = [];
const textElements = container.querySelectorAll(this.options.contentSelector);
textElements.forEach((element) => {
const text = element.textContent || "";
const currencyPatterns = [
/\$[\d,]+/g,
// Dollar signs
/USD\s*[\d,]+/g,
// USD prefix
/[\d,]+\s*USD/g,
// USD suffix
/₲[\d,]+/g,
// Guaraní symbol
/PYG\s*[\d,]+/g,
// PYG prefix
/[\d,]+\s*PYG/g
// PYG suffix
];
currencyPatterns.forEach((pattern) => {
const matches = text.match(pattern);
if (matches) {
matches.forEach((match) => {
if (!this.isValidParaguayanCurrency(match)) {
violations.push({
type: "invalid-pattern",
element,
severity: "moderate",
message: `Currency format "${match}" should follow Paraguay conventions`,
messageEs: `Formato de moneda "${match}" deber\xEDa seguir convenciones de Paraguay`,
currentText: match,
suggestedSpanish: this.formatAsParaguayanCurrency(match),
birhausPrinciple: 7,
context: "financial"
});
}
});
}
});
});
return violations;
}
// Helper methods
checkSpanishLabels(element) {
const spanishAttributes = [
"data-label-es",
"labelEs",
"aria-label",
"title"
];
return spanishAttributes.some((attr) => {
const value = element.getAttribute(attr);
return value && this.isSpanishText(value);
});
}
checkEnglishFallbacks(element) {
const englishAttributes = [
"data-label-en",
"labelEn"
];
return englishAttributes.some((attr) => {
const value = element.getAttribute(attr);
return value && value.length > 0;
});
}
isSpanishText(text) {
if (!text || text.length === 0) return false;
return this.spanishPatterns.some((pattern) => pattern.test(text)) || // Check for Spanish financial terms
Object.values(this.financialTerms).some(
(term) => text.toLowerCase().includes(term.es) || term.alternatives.some((alt) => text.toLowerCase().includes(alt))
);
}
getElementText(element) {
return element.textContent?.trim() || element.getAttribute("aria-label") || element.getAttribute("title") || element.getAttribute("alt") || "";
}
getElementContext(element) {
const tagName = element.tagName.toLowerCase();
const role = element.getAttribute("role");
const className = element.className;
if (tagName === "button" || role === "button") return "button";
if (tagName === "nav" || role === "navigation") return "navigation";
if (["input", "select", "textarea"].includes(tagName)) return "form";
if (className.includes("error") || element.getAttribute("role") === "alert") return "error";
if (this.isFinancialElement(element)) return "financial";
return "content";
}
isFinancialElement(element) {
const text = this.getElementText(element).toLowerCase();
const className = element.className.toLowerCase();
return Object.keys(this.financialTerms).some(
(term) => text.includes(term) || className.includes(term)
);
}
suggestSpanishLabel(element) {
const componentType = element.getAttribute("data-birhaus-component");
const currentText = this.getElementText(element);
if (currentText) {
return this.translateButtonText(currentText);
}
const suggestions = {
"button": "Bot\xF3n",
"input": "Campo de entrada",
"select": "Lista desplegable",
"card": "Tarjeta",
"dialog": "Di\xE1logo",
"table": "Tabla",
"tabs": "Pesta\xF1as",
"checkbox": "Casilla de verificaci\xF3n"
};
return suggestions[componentType || ""] || "Elemento";
}
suggestEnglishLabel(element) {
const spanishText = this.getElementText(element);
return this.translateToEnglish(spanishText);
}
translateButtonText(text) {
const translations = {
"save": "Guardar",
"cancel": "Cancelar",
"edit": "Editar",
"delete": "Eliminar",
"submit": "Enviar",
"reset": "Restablecer",
"close": "Cerrar",
"open": "Abrir",
"add": "Agregar",
"remove": "Quitar",
"approve": "Aprobar",
"reject": "Rechazar",
"export": "Exportar",
"import": "Importar",
"print": "Imprimir",
"search": "Buscar",
"filter": "Filtrar",
"sort": "Ordenar"
};
const lowerText = text.toLowerCase();
return translations[lowerText] || text;
}
translateFormLabel(text) {
const translations = {
"name": "Nombre",
"email": "Correo electr\xF3nico",
"phone": "Tel\xE9fono",
"address": "Direcci\xF3n",
"amount": "Monto",
"date": "Fecha",
"description": "Descripci\xF3n",
"category": "Categor\xEDa",
"status": "Estado",
"type": "Tipo",
"notes": "Notas",
"comments": "Comentarios"
};
const lowerText = text.toLowerCase();
return translations[lowerText] || text;
}
translatePlaceholder(text) {
const translations = {
"enter name": "Ingrese el nombre",
"enter email": "Ingrese el correo electr\xF3nico",
"enter amount": "Ingrese el monto",
"select date": "Seleccione la fecha",
"search...": "Buscar...",
"type here": "Escriba aqu\xED",
"optional": "Opcional"
};
const lowerText = text.toLowerCase();
return translations[lowerText] || text;
}
translateErrorMessage(text) {
const translations = {
"required field": "Campo obligatorio. Complete este campo para continuar.",
"invalid email": "Correo electr\xF3nico inv\xE1lido. Verifique el formato (ejemplo@dominio.com).",
"invalid amount": "Monto inv\xE1lido. Ingrese un n\xFAmero v\xE1lido mayor a cero.",
"network error": "Error de conexi\xF3n. Verifique su conexi\xF3n a internet e intente nuevamente.",
"server error": "Error del servidor. El problema ser\xE1 resuelto pronto, intente m\xE1s tarde."
};
const lowerText = text.toLowerCase();
return translations[lowerText] || `Error: ${text}. Verifique los datos e intente nuevamente.`;
}
translateToEnglish(spanishText) {
const reverseTranslations = {
"guardar": "Save",
"cancelar": "Cancel",
"editar": "Edit",
"eliminar": "Delete",
"enviar": "Submit",
"cerrar": "Close",
"abrir": "Open",
"agregar": "Add",
"quitar": "Remove"
};
const lowerText = spanishText.toLowerCase();
return reverseTranslations[lowerText] || spanishText;
}
hasProperErrorPattern(text) {
const hasExplanation = /error|problema|fallo|inválido|incorrecto/i.test(text);
const hasSolution = /verifique|intente|complete|corrija|revise/i.test(text);
return hasExplanation && hasSolution;
}
improveErrorPattern(text) {
const solutions = [
"Verifique los datos e intente nuevamente.",
"Complete la informaci\xF3n requerida.",
"Revise el formato e intente otra vez.",
"Intente m\xE1s tarde o contacte soporte."
];
const randomSolution = solutions[Math.floor(Math.random() * solutions.length)];
return `${text} ${randomSolution}`;
}
getAssociatedLabel(element) {
const labelledBy = element.getAttribute("aria-labelledby");
if (labelledBy) {
return document.getElementById(labelledBy);
}
const id = element.getAttribute("id");
if (id) {
return document.querySelector(`label[for="${id}"]`);
}
return element.closest("label");
}
isValidParaguayanCurrency(currencyText) {
const paraguayanPatterns = [
/^₲\s?\d{1,3}(\.\d{3})*$/,
/^PYG\s\d{1,3}(\.\d{3})*$/,
/^\d{1,3}(\.\d{3})*\sPYG$/
];
return paraguayanPatterns.some((pattern) => pattern.test(currencyText));
}
formatAsParaguayanCurrency(currencyText) {
const number = currencyText.replace(/[^\d]/g, "");
const formatted = new Intl.NumberFormat("es-PY").format(parseInt(number));
return `\u20B2 ${formatted}`;
}
};
function expectSpanishFirst(container, strict = true) {
const validator = new SpanishCoverageValidator({
requireSpanishPrimary: true,
strictFinancialTerms: strict
});
const violations = validator.validateAll(container);
const seriousViolations = violations.filter((v) => v.severity === "serious" || v.severity === "critical");
if (seriousViolations.length > 0) {
const errorMessages = seriousViolations.map((v) => v.messageEs).join("\n");
throw new Error(
`Spanish-first violations detected:
${errorMessages}
Total violations: ${violations.length} (${seriousViolations.length} serious)`
);
}
}
function expectBilingualCompleteness(container) {
const validator = new SpanishCoverageValidator({
requireSpanishPrimary: true,
requireEnglishFallback: true
});
const violations = validator.validateAll(container);
if (violations.length > 0) {
const errorMessages = violations.map((v) => v.message).join("\n");
throw new Error(
`Bilingual completeness violations detected:
${errorMessages}`
);
}
}
function expectFinancialSpanish(container) {
const validator = new SpanishCoverageValidator({
validateFinancialVocabulary: true,
strictFinancialTerms: true
});
const violations = validator.validateFinancialTerminology(container || document.body);
if (violations.length > 0) {
const errorMessages = violations.map((v) => `${v.currentText} \u2192 ${v.suggestedSpanish}`).join("\n");
throw new Error(
`Financial terminology violations detected:
${errorMessages}`
);
}
}
exports.SpanishCoverageValidator = SpanishCoverageValidator;
exports.expectBilingualCompleteness = expectBilingualCompleteness;
exports.expectFinancialSpanish = expectFinancialSpanish;
exports.expectSpanishFirst = expectSpanishFirst;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map