Skip to content
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

chore: validate json files #20120

Merged
merged 12 commits into from
Mar 31, 2025
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"build:epd-visualization": "npm --prefix integration-libs/epd-visualization run build:schematics && nx build epd-visualization --configuration production",
"build:estimated-delivery-date": "npm --prefix feature-libs/estimated-delivery-date run build:schematics && nx build estimated-delivery-date --configuration production",
"build:order": "npm --prefix feature-libs/order run build:schematics && nx build order --configuration production",
"build:libs": "nx build core --configuration production && nx build storefrontlib --configuration production && concurrently --kill-others-on-fail npm:build:schematics npm:build:user && npm run build:cart && npm run build:pdf-invoices && npm run build:order && npm run build:storefinder && concurrently --kill-others-on-fail npm:build:checkout npm:build:asm npm:build:tracking npm:build:customer-ticketing && concurrently --kill-others-on-fail npm:build:organization npm:build:product npm:build:product-configurator npm:build:product-multi-dimensional && concurrently --kill-others-on-fail npm:build:requested-delivery-date && concurrently --kill-others-on-fail npm:build:estimated-delivery-date && concurrently --kill-others-on-fail npm:build:smartedit npm:build:qualtrics npm:build:assets npm:build:cds npm:build:cdc npm:build:cdp npm:build:digital-payments npm:build:epd-visualization npm:build:s4om npm:build:omf npm:build:cpq-quote npm:build:segment-refs npm:build:opf npm:build:opps npm:build:pickup-in-store npm:build:quote && npm run build:setup && npm run build:s4-service",
"build:libs": "ts-node ./scripts/i18n/validate-translations-json-files.ts && nx build core --configuration production && nx build storefrontlib --configuration production && concurrently --kill-others-on-fail npm:build:schematics npm:build:user && npm run build:cart && npm run build:pdf-invoices && npm run build:order && npm run build:storefinder && concurrently --kill-others-on-fail npm:build:checkout npm:build:asm npm:build:tracking npm:build:customer-ticketing && concurrently --kill-others-on-fail npm:build:organization npm:build:product npm:build:product-configurator npm:build:product-multi-dimensional && concurrently --kill-others-on-fail npm:build:requested-delivery-date && concurrently --kill-others-on-fail npm:build:estimated-delivery-date && concurrently --kill-others-on-fail npm:build:smartedit npm:build:qualtrics npm:build:assets npm:build:cds npm:build:cdc npm:build:cdp npm:build:digital-payments npm:build:epd-visualization npm:build:s4om npm:build:omf npm:build:cpq-quote npm:build:segment-refs npm:build:opf npm:build:opps npm:build:pickup-in-store npm:build:quote && npm run build:setup && npm run build:s4-service",
"build:organization": "npm --prefix feature-libs/organization run build:schematics && nx build organization --configuration production",
"build:pdf-invoices": "npm --prefix feature-libs/pdf-invoices run build:schematics && nx build pdf-invoices --configuration production",
"build:pickup-in-store": "npm --prefix feature-libs/pickup-in-store run build:schematics && nx build pickup-in-store --configuration production",
Expand Down
117 changes: 117 additions & 0 deletions scripts/i18n/validate-translations-json-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import * as fs from 'fs-extra';

const MAX_FILE_SIZE = 50 * 1024; // 50 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;

export 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 = `${dir}/${file}`;
const stat = fs.statSync(filePath);

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

return results;
}

function validateAllTranslations() {
/* eslint-disable-next-line no-console */
console.log('Validating translation files...');

const basePaths: string[] = ['feature-libs', 'integration-libs', 'projects'];

basePaths.forEach((basePath: string) => {
const jsonFilePaths = getJsonFiles(basePath);

jsonFilePaths.forEach((path: string) => {
validateJson(path);
});
});

/* eslint-disable-next-line no-console */
console.log('✅ All translations passed validation!');
}

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