Skip to content

chore: validate json files #20120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 31, 2025
Merged
5 changes: 3 additions & 2 deletions projects/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
"homepage": "https://github.com/SAP/spartacus",
"repository": "https://github.com/SAP/spartacus/tree/develop/projects/assets",
"scripts": {
"build": "nx build assets --configuration production && npm run generate:translations:ts-2-json",
"build": "nx build assets --configuration production && npm run generate:translations:ts-2-json && npm run validate:json",
"generate:translations:properties-2-ts": "ts-node ./generate-translations-properties-2-ts && cd ../.. && npx prettier \"./projects/assets/src/translations/**/*.ts\" --write",
"generate:translations:ts-2-json": "ts-node ./generate-translations-ts-2-json",
"generate:translations:ts-2-properties": "ts-node ./generate-translations-ts-2-properties"
"generate:translations:ts-2-properties": "ts-node ./generate-translations-ts-2-properties",
"validate:json": "ts-node ./validate-translations-json-files.ts"
},
"dependencies": {
"tslib": "^2.8.1"
Expand Down
104 changes: 104 additions & 0 deletions projects/assets/validate-translations-json-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import fs from 'fs';
import path from 'path';

const TRANSLATIONS_DIR = '../assets/src/translations';
const MAX_FILE_SIZE = 20 * 1024; // 20 KB limit
const BANNED_KEYS = [
'<script',
'onerror',
'onload',
'onclick',
'javascript:',
'__proto__',
'constructor',
'eval',
];
const NOT_ALLOWED_VALUE_REGEX =
/^[{\[](?:[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]|".*?")*[}\]]$/u;

function validateJson(filePath: string) {
const fileSize = fs.statSync(filePath).size;

if (fileSize > MAX_FILE_SIZE) {
throw new Error(`File ${filePath} exceeds size limit (${fileSize} bytes).`);
}

const content = fs.readFileSync(filePath, 'utf8');

let parsedJson;
try {
parsedJson = JSON.parse(content);
} catch (error) {
throw new Error(`Invalid JSON format in ${filePath}`);
}

const traverse = (jsonData: any) => {
for (const key in jsonData) {
if (typeof jsonData[key] === 'object' && jsonData[key] !== null) {
traverse(jsonData[key]);
} else {
const translation = jsonData[key];

if (typeof translation !== 'string') {
throw new Error(`Translation entry is not a string.`);
}

if (NOT_ALLOWED_VALUE_REGEX.test(translation) || NOT_ALLOWED_VALUE_REGEX.test(key) ) {
throw new Error(
`Unallowed char in ${filePath}: ${key} '${translation}'.`
);
}

if (BANNED_KEYS.some((bannedKey) => translation.includes(bannedKey) || key.includes(bannedKey))) {
throw new Error(
`Forbidden char in ${filePath}: ${key} '${translation}'.`
);
}
}
}
};
traverse(parsedJson);
}

function getJsonFiles(dir: any) {
let results: any[] = [];
const list = fs.readdirSync(dir);

list.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (stat && stat.isDirectory()) {
results = results.concat(getJsonFiles(filePath));
} else if (file.endsWith('.json')) {
results.push(filePath);
}
});

return results;
}

function validateAllTranslations() {
console.log('Validating translation files...');

const filePaths = getJsonFiles(TRANSLATIONS_DIR);

filePaths.forEach((filePath) => {
validateJson(filePath);
});

console.log('✅ All translations passed validation!');
}

try {
validateAllTranslations();
} catch (error) {
console.error('❌ Validation failed: ', error);
process.exit(1); // Exit with error status to stop the build
}
Loading