UNPKG

@blaze-money/eslint-plugin-spark

Version:

ESLint plugin for Spark project with custom rules

204 lines (179 loc) 6.22 kB
/** * @fileoverview Rule to ensure all email translation files have matching structures * @author Blaze Team */ "use strict" const fs = require("fs") const path = require("path") /** * Deep comparison of two objects to check if they have the same structure * @param {Object} obj1 - First object to compare * @param {Object} obj2 - Second object to compare * @returns {boolean} - True if objects have the same structure */ const deepEqual = (obj1, obj2) => { if (typeof obj1 !== typeof obj2) return false if (typeof obj1 !== "object") return true if (!obj1 || !obj2) return obj1 === obj2 const keys1 = Object.keys(obj1) const keys2 = Object.keys(obj2) if (keys1.length !== keys2.length) return false return keys1.every(key => { if (!obj2.hasOwnProperty(key)) return false return deepEqual(obj1[key], obj2[key]) }) } /** * Find structural differences between two objects * @param {Object} baseObj - Base object to compare against (usually English translations) * @param {Object} compareObj - Object to compare with the base * @param {Array} path - Current path in the object (for nested objects) * @returns {Array} - Array of differences with type and path */ const getStructuralDifferences = (baseObj, compareObj, path = []) => { const differences = [] // Check for missing keys in translation Object.keys(baseObj).forEach(key => { const currentPath = [...path, key] if (!compareObj.hasOwnProperty(key)) { differences.push({ type: "missing", path: currentPath.join("."), }) return } if (typeof baseObj[key] === "object" && baseObj[key] !== null) { differences.push( ...getStructuralDifferences(baseObj[key], compareObj[key], currentPath) ) } }) // Check for extra keys in translation Object.keys(compareObj).forEach(key => { const currentPath = [...path, key] if (!baseObj.hasOwnProperty(key)) { differences.push({ type: "extra", path: currentPath.join("."), }) } }) return differences } module.exports = { meta: { type: "problem", docs: { description: "ensure non-English email translation files match the structure of en.messages.ts", category: "Possible Errors", recommended: true, }, fixable: null, schema: [], // no options }, create(context) { return { Program(node) { const filename = context.getFilename() // Only check email translation message files if ( !filename.endsWith(".messages.ts") || (!filename.includes("/emails/lang/") && !filename.includes("/Email/emails/lang/")) || filename.endsWith("/locale.messages.ts") ) { return } // Skip checking en.messages.ts as it's our base if (filename.endsWith("/en.messages.ts")) { return } try { const sourceCode = context.getSourceCode().text // Extract the messages object from the file content // This is a simple regex approach - in a real implementation, you might want to use a TS parser const messagesMatch = sourceCode.match( /export\s+const\s+messages\s*=\s*({[\s\S]*})/m ) if (!messagesMatch || !messagesMatch[1]) { context.report({ node, message: "Could not parse messages object from translation file", }) return } // Get the path to en.messages.ts const enPath = filename.replace( /[^/]+\.messages\.ts$/, "en.messages.ts" ) const fullEnPath = path.resolve(enPath) try { if (!fs.existsSync(fullEnPath)) { context.report({ node, message: `Could not find English translation file at ${enPath}`, }) return } const enContent = fs.readFileSync(fullEnPath, "utf8") const enMessagesMatch = enContent.match( /export\s+const\s+messages\s*=\s*({[\s\S]*})/m ) if (!enMessagesMatch || !enMessagesMatch[1]) { context.report({ node, message: "Could not parse messages object from English translation file", }) return } // Convert the string representation to actual objects // Note: This is a simplified approach using eval, which has security implications // In a real implementation, you might want to use a safer approach let translationMessages, enMessages try { // Using Function constructor is safer than eval translationMessages = new Function(`return ${messagesMatch[1]}`)() enMessages = new Function(`return ${enMessagesMatch[1]}`)() } catch (evalError) { context.report({ node, message: `Error evaluating messages object: ${evalError.message}`, }) return } const differences = getStructuralDifferences( enMessages, translationMessages ) if (differences.length > 0) { const messages = differences.map(diff => { if (diff.type === "missing") { return `Missing key: ${diff.path}` } else { return `Extra key not in en.messages.ts: ${diff.path}` } }) context.report({ node, message: `Translation file structure does not match en.messages.ts:\n${messages.join("\n")}`, }) } } catch (error) { context.report({ node, message: `Error comparing translation files: ${error.message}`, }) } } catch (error) { context.report({ node, message: `Error processing translation file: ${error.message}`, }) } }, } }, }