sicua
Version:
A tool for analyzing project structure and dependencies
223 lines (222 loc) • 8.38 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findTranslationFiles = findTranslationFiles;
exports.hasNestedStringValues = hasNestedStringValues;
exports.isLikelyTranslationFile = isLikelyTranslationFile;
exports.countTranslationEntries = countTranslationEntries;
exports.determineMainTranslationFile = determineMainTranslationFile;
exports.translationExistsInObject = translationExistsInObject;
exports.extractKeysFromTranslations = extractKeysFromTranslations;
exports.flattenTranslations = flattenTranslations;
exports.isSignificantText = isSignificantText;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const pathUtils_1 = require("../../../utils/common/pathUtils");
/**
* Finds all translation files in the project
* @param context Translation file context
* @returns Promise that resolves to an array of translation files
*/
async function findTranslationFiles(context) {
const translationFiles = [];
const { projectPath, log } = context;
// Check common locations for translation files
const possibleLocations = [
"messages", // next-intl
"locales", // next-i18next
"translations", // common folder name
"i18n/locales", // i18next
"src/locales", // common location in src
"public/locales", // another common location
];
for (const location of possibleLocations) {
const dirPath = path_1.default.join(projectPath, location);
log(`Checking for translation directory at: ${dirPath}`);
if (!fs_1.default.existsSync(dirPath)) {
continue;
}
if (!fs_1.default.statSync(dirPath).isDirectory()) {
continue;
}
// Search for translation files
const foundFiles = await findFilesInDirectory(dirPath, context);
translationFiles.push(...foundFiles);
// If we found files in one location, don't check others
if (foundFiles.length > 0) {
log(`Found ${foundFiles.length} translation files in ${dirPath}`);
break;
}
}
return translationFiles;
}
/**
* Finds translation files in a directory
* @param dirPath Directory path
* @param context Translation file context
* @returns Promise that resolves to an array of translation files
*/
async function findFilesInDirectory(dirPath, context) {
const { log } = context;
const translationFiles = [];
try {
const files = fs_1.default.readdirSync(dirPath);
log(`Found ${files.length} files in directory: ${dirPath}`);
for (const file of files) {
const filePath = path_1.default.join(dirPath, file);
const stat = fs_1.default.statSync(filePath);
if (stat.isDirectory()) {
// Recursively check subdirectories
const nestedFiles = await findFilesInDirectory(filePath, context);
translationFiles.push(...nestedFiles);
}
else if (file.endsWith(".json")) {
// Process JSON files
try {
log(`Reading translation file: ${filePath}`);
const content = await (0, pathUtils_1.readJsonFile)(filePath);
// Check if this looks like a translation file
if (isLikelyTranslationFile(content)) {
const size = countTranslationEntries(content);
log(`Found ${size} translation entries in ${filePath}`);
translationFiles.push({
path: filePath,
content,
size,
});
}
}
catch (error) {
log(`Error reading translation file ${filePath}: ${error}`);
}
}
}
}
catch (error) {
log(`Error reading directory ${dirPath}: ${error}`);
}
return translationFiles;
}
function hasNestedStringValues(node) {
if (typeof node === "string") {
return true;
}
if (typeof node === "object" && node !== null) {
return Object.values(node).some(hasNestedStringValues);
}
return false;
}
/**
* Determines if a JSON object is likely a translation file
* @param content JSON content
* @returns Boolean indicating if it looks like a translation file
*/
function isLikelyTranslationFile(content) {
if (Object.keys(content).length === 0) {
return false;
}
return hasNestedStringValues(content);
}
/**
* Counts the number of translation entries in a file (recursive)
* @param obj Translation object
* @param prefix Optional prefix for recursive calls
* @returns Number of translations
*/
function countTranslationEntries(obj, prefix = "") {
let count = 0;
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
count += countTranslationEntries(value, fullKey);
}
else {
count++;
}
}
return count;
}
/**
* Determines the main translation file (largest one)
* @param translationFiles Array of translation files
* @returns The largest translation file or null if none found
*/
function determineMainTranslationFile(translationFiles) {
if (translationFiles.length === 0)
return null;
return translationFiles.reduce((largest, current) => (current.size > largest.size ? current : largest), translationFiles[0]);
}
/**
* Checks if a translation key exists in the translation object
* @param fullKey The full dot-notation key
* @param translations Translation object
* @returns Boolean indicating if the key exists
*/
function translationExistsInObject(fullKey, translations) {
const parts = fullKey.split(".");
let current = translations;
for (const part of parts) {
if (!current || typeof current !== "object" || !(part in current)) {
return false;
}
current = current[part];
}
return typeof current === "string";
}
/**
* Extracts all translation keys from a translation object
* @param obj Translation object
* @param prefix Optional prefix for recursive calls
* @param result Array to collect results
*/
function extractKeysFromTranslations(obj, prefix, result) {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
extractKeysFromTranslations(value, fullKey, result);
}
else if (typeof value === "string") {
result.push(fullKey);
}
}
}
/**
* Flattens a nested translation object for comparison
* @param obj Translation object
* @param prefix Optional prefix for recursive calls
* @param filePath File path of the translation file
* @param valueToKeysMap Map to collect values and their keys
*/
function flattenTranslations(obj, prefix, filePath, valueToKeysMap) {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
flattenTranslations(value, fullKey, filePath, valueToKeysMap);
}
else if (typeof value === "string") {
const existing = valueToKeysMap.get(value) || [];
existing.push({ fullKey, filePath });
valueToKeysMap.set(value, existing);
}
}
}
/**
* Checks if text is significant enough to warn about duplication
* @param value The text value to check
* @returns Boolean indicating if the text is significant
*/
function isSignificantText(value) {
// Ignore very short strings or non-significant content
if (value.length < 5)
return false;
// Ignore strings that are just "yes", "no", "true", "false"
const lowered = value.toLowerCase();
if (["yes", "no", "true", "false", "ok", "cancel"].includes(lowered))
return false;
// Ignore strings that are just numbers
if (/^\d+$/.test(value))
return false;
return true;
}