@blaze-money/eslint-plugin-spark
Version:
ESLint plugin for Spark project with custom rules
204 lines (179 loc) • 6.22 kB
JavaScript
/**
* @fileoverview Rule to ensure all email translation files have matching structures
* @author Blaze Team
*/
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}`,
})
}
},
}
},
}