mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
chore(client): ts-migrate i18n-schema-validation (#46703)
* chore(client): Typescript migrate of i18n-schema-validation * refactor(client): readJsonFile in i18n-schema-validation * chore(client): extract field
This commit is contained in:
@@ -1,287 +0,0 @@
|
||||
const path = require('path');
|
||||
const { availableLangs } = require('../../config/i18n/all-langs');
|
||||
const introSchema = require('./locales/english/intro.json');
|
||||
const linksSchema = require('./locales/english/links.json');
|
||||
const metaTagsSchema = require('./locales/english/meta-tags.json');
|
||||
const motivationSchema = require('./locales/english/motivation.json');
|
||||
const translationsSchema = require('./locales/english/translations.json');
|
||||
const trendingSchema = require('./locales/english/trending.json');
|
||||
|
||||
/**
|
||||
* Flattens a nested object structure into a single
|
||||
* object with property chains as keys.
|
||||
* @param {Object} obj Object to flatten
|
||||
* @param {String} namespace Used for property chaining
|
||||
*/
|
||||
const flattenAnObject = (obj, namespace = '') => {
|
||||
const flattened = {};
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (Array.isArray(obj[key])) {
|
||||
flattened[namespace ? `${namespace}.${key}` : key] = obj[key];
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
Object.assign(
|
||||
flattened,
|
||||
flattenAnObject(obj[key], namespace ? `${namespace}.${key}` : key)
|
||||
);
|
||||
} else {
|
||||
flattened[namespace ? `${namespace}.${key}` : key] = obj[key];
|
||||
}
|
||||
});
|
||||
return flattened;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a translation object is missing keys
|
||||
* that are present in the schema.
|
||||
* @param {String[]} file Array of translation object's keys
|
||||
* @param {String[]} schema Array of matching schema's keys
|
||||
* @param {String} path string path to file
|
||||
*/
|
||||
const findMissingKeys = (file, schema, path) => {
|
||||
const missingKeys = [];
|
||||
for (const key of schema) {
|
||||
if (!file.includes(key)) {
|
||||
missingKeys.push(key);
|
||||
}
|
||||
}
|
||||
if (missingKeys.length) {
|
||||
console.warn(
|
||||
`${path} is missing these required keys: ${missingKeys.join(', ')}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a translation object has extra
|
||||
* keys which are NOT present in the schema.
|
||||
* @param {String[]} file Array of translation object's keys
|
||||
* @param {String[]} schema Array of matching schema's keys
|
||||
* @param {String} path string path to file
|
||||
*/
|
||||
const findExtraneousKeys = (file, schema, path) => {
|
||||
const extraKeys = [];
|
||||
for (const key of file) {
|
||||
if (!schema.includes(key)) {
|
||||
extraKeys.push(key);
|
||||
}
|
||||
}
|
||||
if (extraKeys.length) {
|
||||
console.warn(
|
||||
`${path} has these keys that are not in the schema: ${extraKeys.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that all values in the object are non-empty. Includes
|
||||
* validation of nested objects.
|
||||
* @param {Object} obj The object to check the values of
|
||||
* @param {String} namespace String for tracking nested properties
|
||||
*/
|
||||
const noEmptyObjectValues = (obj, namespace = '') => {
|
||||
const emptyKeys = [];
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (Array.isArray(obj[key])) {
|
||||
if (!obj[key].length) {
|
||||
emptyKeys.push(namespace ? `${namespace}.${key}` : key);
|
||||
}
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
emptyKeys.push(
|
||||
noEmptyObjectValues(obj[key], namespace ? `${namespace}.${key}` : key)
|
||||
);
|
||||
} else if (!obj[key]) {
|
||||
emptyKeys.push(namespace ? `${namespace}.${key}` : key);
|
||||
}
|
||||
}
|
||||
return emptyKeys.flat();
|
||||
};
|
||||
|
||||
/**
|
||||
* Grab the schema keys once, to avoid overhead of
|
||||
* fetching within iterative function.
|
||||
*/
|
||||
const translationSchemaKeys = Object.keys(flattenAnObject(translationsSchema));
|
||||
const trendingSchemaKeys = Object.keys(flattenAnObject(trendingSchema));
|
||||
const motivationSchemaKeys = Object.keys(flattenAnObject(motivationSchema));
|
||||
const introSchemaKeys = Object.keys(flattenAnObject(introSchema));
|
||||
const metaTagsSchemaKeys = Object.keys(flattenAnObject(metaTagsSchema));
|
||||
const linksSchemaKeys = Object.keys(flattenAnObject(linksSchema));
|
||||
|
||||
/**
|
||||
* Function that checks the translations.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const translationSchemaValidation = languages => {
|
||||
languages.forEach(language => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
`/locales/${language}/translations.json`
|
||||
);
|
||||
const fileJson = require(filePath);
|
||||
const fileKeys = Object.keys(flattenAnObject(fileJson));
|
||||
findMissingKeys(
|
||||
fileKeys,
|
||||
translationSchemaKeys,
|
||||
`${language}/translations.json`
|
||||
);
|
||||
findExtraneousKeys(
|
||||
fileKeys,
|
||||
translationSchemaKeys,
|
||||
`${language}/translations.json`
|
||||
);
|
||||
const emptyKeys = noEmptyObjectValues(fileJson);
|
||||
if (emptyKeys.length) {
|
||||
console.warn(
|
||||
`${language}/translations.json has these empty keys: ${emptyKeys.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
console.info(`${language} translations.json validation complete.`);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that checks the trending.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const trendingSchemaValidation = languages => {
|
||||
languages.forEach(language => {
|
||||
const filePath = path.join(__dirname, `/locales/${language}/trending.json`);
|
||||
const fileJson = require(filePath);
|
||||
const fileKeys = Object.keys(flattenAnObject(fileJson));
|
||||
findMissingKeys(fileKeys, trendingSchemaKeys, `${language}/trending.json`);
|
||||
findExtraneousKeys(
|
||||
fileKeys,
|
||||
trendingSchemaKeys,
|
||||
`${language}/trending.json`
|
||||
);
|
||||
const emptyKeys = noEmptyObjectValues(fileJson);
|
||||
if (emptyKeys.length) {
|
||||
console.warn(
|
||||
`${language}/trending.json has these empty keys: ${emptyKeys.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
console.info(`${language} trending.json validation complete`);
|
||||
});
|
||||
};
|
||||
|
||||
const motivationSchemaValidation = languages => {
|
||||
languages.forEach(language => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
`/locales/${language}/motivation.json`
|
||||
);
|
||||
const fileJson = require(filePath);
|
||||
const fileKeys = Object.keys(flattenAnObject(fileJson));
|
||||
findMissingKeys(
|
||||
fileKeys,
|
||||
motivationSchemaKeys,
|
||||
`${language}/motivation.json`
|
||||
);
|
||||
findExtraneousKeys(
|
||||
fileKeys,
|
||||
motivationSchemaKeys,
|
||||
`${language}/motivation.json`
|
||||
);
|
||||
const emptyKeys = noEmptyObjectValues(fileJson);
|
||||
if (emptyKeys.length) {
|
||||
console.warn(
|
||||
`${language}/motivation.json has these empty keys: ${emptyKeys.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
// Special line to assert that objects in motivational quote are correct
|
||||
if (
|
||||
!fileJson.motivationalQuotes.every(
|
||||
object =>
|
||||
Object.prototype.hasOwnProperty.call(object, 'quote') &&
|
||||
Object.prototype.hasOwnProperty.call(object, 'author')
|
||||
)
|
||||
) {
|
||||
console.warn(`${language}/motivation.json has malformed quote objects.`);
|
||||
}
|
||||
console.info(`${language} motivation.json validation complete`);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that checks the intro.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const introSchemaValidation = languages => {
|
||||
languages.forEach(language => {
|
||||
const filePath = path.join(__dirname, `/locales/${language}/intro.json`);
|
||||
const fileJson = require(filePath);
|
||||
const fileKeys = Object.keys(flattenAnObject(fileJson));
|
||||
findMissingKeys(fileKeys, introSchemaKeys, `${language}/intro.json`);
|
||||
findExtraneousKeys(fileKeys, introSchemaKeys, `${language}/intro.json`);
|
||||
const emptyKeys = noEmptyObjectValues(fileJson);
|
||||
if (emptyKeys.length) {
|
||||
console.warn(
|
||||
`${language}/intro.json has these empty keys: ${emptyKeys.join(', ')}`
|
||||
);
|
||||
}
|
||||
console.info(`${language} intro.json validation complete`);
|
||||
});
|
||||
};
|
||||
|
||||
const metaTagsSchemaValidation = languages => {
|
||||
languages.forEach(language => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
`/locales/${language}/meta-tags.json`
|
||||
);
|
||||
const fileJson = require(filePath);
|
||||
const fileKeys = Object.keys(flattenAnObject(fileJson));
|
||||
findMissingKeys(fileKeys, metaTagsSchemaKeys, `${language}/meta-tags.json`);
|
||||
findExtraneousKeys(
|
||||
fileKeys,
|
||||
metaTagsSchemaKeys,
|
||||
`${language}/metaTags.json`
|
||||
);
|
||||
const emptyKeys = noEmptyObjectValues(fileJson);
|
||||
if (emptyKeys.length) {
|
||||
console.warn(
|
||||
`${language}/metaTags.json has these empty keys: ${emptyKeys.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
console.info(`${language} metaTags.json validation complete`);
|
||||
});
|
||||
};
|
||||
|
||||
const linksSchemaValidation = languages => {
|
||||
languages.forEach(language => {
|
||||
const filePath = path.join(__dirname, `/locales/${language}/links.json`);
|
||||
const fileJson = require(filePath);
|
||||
const fileKeys = Object.keys(flattenAnObject(fileJson));
|
||||
findMissingKeys(fileKeys, linksSchemaKeys, `${language}/links.json`);
|
||||
findExtraneousKeys(fileKeys, linksSchemaKeys, `${language}/links.json`);
|
||||
const emptyKeys = noEmptyObjectValues(fileJson);
|
||||
if (emptyKeys.length) {
|
||||
console.warn(
|
||||
`${language}/links.json has these empty keys: ${emptyKeys.join(', ')}`
|
||||
);
|
||||
}
|
||||
console.info(`${language} links.json validation complete`);
|
||||
});
|
||||
};
|
||||
|
||||
const translatedLangs = availableLangs.client.filter(x => x !== 'english');
|
||||
|
||||
translationSchemaValidation(translatedLangs);
|
||||
trendingSchemaValidation(translatedLangs);
|
||||
motivationSchemaValidation(translatedLangs);
|
||||
introSchemaValidation(translatedLangs);
|
||||
metaTagsSchemaValidation(translatedLangs);
|
||||
linksSchemaValidation(translatedLangs);
|
||||
@@ -0,0 +1,260 @@
|
||||
import path from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { availableLangs } from '../../config/i18n/all-langs';
|
||||
import introSchema from './locales/english/intro.json';
|
||||
import linksSchema from './locales/english/links.json';
|
||||
import metaTagsSchema from './locales/english/meta-tags.json';
|
||||
import motivationSchema from './locales/english/motivation.json';
|
||||
import translationsSchema from './locales/english/translations.json';
|
||||
import trendingSchema from './locales/english/trending.json';
|
||||
|
||||
type MotivationalQuotes = { quote: string; author: string }[];
|
||||
|
||||
/**
|
||||
* Flattens a nested object structure into a single
|
||||
* object with property chains as keys.
|
||||
* @param {Object} obj Object to flatten
|
||||
* @param {String} namespace Used for property chaining
|
||||
*/
|
||||
const flattenAnObject = (obj: Record<string, unknown>, namespace = '') => {
|
||||
const flattened: Record<string, unknown> = {};
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
const field = namespace ? `${namespace}.${key}` : key;
|
||||
if (Array.isArray(value)) {
|
||||
flattened[field] = value;
|
||||
} else if (typeof value === 'object') {
|
||||
Object.assign(
|
||||
flattened,
|
||||
flattenAnObject(value as Record<string, unknown>, field)
|
||||
);
|
||||
} else {
|
||||
flattened[field] = value;
|
||||
}
|
||||
});
|
||||
return flattened;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a translation object is missing keys
|
||||
* that are present in the schema.
|
||||
* @param {String[]} file Array of translation object's keys
|
||||
* @param {String[]} schema Array of matching schema's keys
|
||||
* @param {String} path string path to file
|
||||
*/
|
||||
const findMissingKeys = (file: string[], schema: string[], path: string) => {
|
||||
const missingKeys = [];
|
||||
for (const key of schema) {
|
||||
if (!file.includes(key)) {
|
||||
missingKeys.push(key);
|
||||
}
|
||||
}
|
||||
if (missingKeys.length) {
|
||||
console.warn(
|
||||
`${path} is missing these required keys: ${missingKeys.join(', ')}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a translation object has extra
|
||||
* keys which are NOT present in the schema.
|
||||
* @param {String[]} file Array of translation object's keys
|
||||
* @param {String[]} schema Array of matching schema's keys
|
||||
* @param {String} path string path to file
|
||||
*/
|
||||
const findExtraneousKeys = (file: string[], schema: string[], path: string) => {
|
||||
const extraKeys = [];
|
||||
for (const key of file) {
|
||||
if (!schema.includes(key)) {
|
||||
extraKeys.push(key);
|
||||
}
|
||||
}
|
||||
if (extraKeys.length) {
|
||||
console.warn(
|
||||
`${path} has these keys that are not in the schema: ${extraKeys.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that all values in the object are non-empty. Includes
|
||||
* validation of nested objects.
|
||||
* @param {Object} obj The object to check the values of
|
||||
* @param {String} namespace String for tracking nested properties
|
||||
*/
|
||||
const noEmptyObjectValues = (
|
||||
obj: Record<string, unknown>,
|
||||
namespace = ''
|
||||
): string[] => {
|
||||
const emptyKeys = [];
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
const field = namespace ? `${namespace}.${key}` : key;
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) {
|
||||
emptyKeys.push(field);
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
emptyKeys.push(
|
||||
noEmptyObjectValues(value as Record<string, unknown>, field)
|
||||
);
|
||||
} else if (!value) {
|
||||
emptyKeys.push(field);
|
||||
}
|
||||
}
|
||||
return emptyKeys.flat();
|
||||
};
|
||||
|
||||
/**
|
||||
* Grab the schema keys once, to avoid overhead of
|
||||
* fetching within iterative function.
|
||||
*/
|
||||
const translationSchemaKeys = Object.keys(flattenAnObject(translationsSchema));
|
||||
const trendingSchemaKeys = Object.keys(flattenAnObject(trendingSchema));
|
||||
const motivationSchemaKeys = Object.keys(flattenAnObject(motivationSchema));
|
||||
const introSchemaKeys = Object.keys(flattenAnObject(introSchema));
|
||||
const metaTagsSchemaKeys = Object.keys(flattenAnObject(metaTagsSchema));
|
||||
const linksSchemaKeys = Object.keys(flattenAnObject(linksSchema));
|
||||
|
||||
/**
|
||||
* Function that checks the translations.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const translationSchemaValidation = (languages: string[]) => {
|
||||
languages.forEach(language => {
|
||||
void readJsonFile(language, 'translations').then(fileJson => {
|
||||
schemaValidation(
|
||||
language,
|
||||
'translations',
|
||||
fileJson,
|
||||
translationSchemaKeys
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that checks the trending.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const trendingSchemaValidation = (languages: string[]) => {
|
||||
languages.forEach(language => {
|
||||
void readJsonFile(language, 'trending').then(fileJson => {
|
||||
schemaValidation(language, 'trending', fileJson, trendingSchemaKeys);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that checks the motivation.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const motivationSchemaValidation = (languages: string[]) => {
|
||||
languages.forEach(language => {
|
||||
void readJsonFile(language, 'motivation').then(fileJson => {
|
||||
schemaValidation(language, 'motivation', fileJson, motivationSchemaKeys);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that checks the intro.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const introSchemaValidation = (languages: string[]) => {
|
||||
languages.forEach(language => {
|
||||
void readJsonFile(language, 'intro').then(fileJson => {
|
||||
schemaValidation(language, 'intro', fileJson, introSchemaKeys);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that checks the meta-tags.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const metaTagsSchemaValidation = (languages: string[]) => {
|
||||
languages.forEach(language => {
|
||||
void readJsonFile(language, 'meta-tags').then(fileJson => {
|
||||
schemaValidation(language, 'meta-tags', fileJson, metaTagsSchemaKeys);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that checks the links.json file
|
||||
* for each available client language.
|
||||
* @param {String[]} languages List of languages to test
|
||||
*/
|
||||
const linksSchemaValidation = (languages: string[]) => {
|
||||
languages.forEach(language => {
|
||||
void readJsonFile(language, 'links').then(fileJson => {
|
||||
schemaValidation(language, 'links', fileJson, linksSchemaKeys);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Common Function that checks the json file
|
||||
* @param {String} language the language to test
|
||||
* @param {String} fileName the fileName of json file to test
|
||||
* @param {Object} fileJson the fileJson got by readJsonFile
|
||||
* @param {String[]} schemaKeys Array of matching schema's keys
|
||||
*/
|
||||
const schemaValidation = (
|
||||
language: string,
|
||||
fileName: string,
|
||||
fileJson: Record<string, unknown>,
|
||||
schemaKeys: string[]
|
||||
) => {
|
||||
const fileKeys = Object.keys(flattenAnObject(fileJson));
|
||||
findMissingKeys(fileKeys, schemaKeys, `${language}/${fileName}.json`);
|
||||
findExtraneousKeys(fileKeys, schemaKeys, `${language}/${fileName}.json`);
|
||||
const emptyKeys = noEmptyObjectValues(fileJson);
|
||||
if (emptyKeys.length) {
|
||||
console.warn(
|
||||
`${language}/${fileName}.json has these empty keys: ${emptyKeys.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
// Special line to assert that objects in motivational quote are correct
|
||||
if (
|
||||
fileName === 'motivation' &&
|
||||
!(fileJson.motivationalQuotes as MotivationalQuotes).every(
|
||||
(object: object) =>
|
||||
Object.prototype.hasOwnProperty.call(object, 'quote') &&
|
||||
Object.prototype.hasOwnProperty.call(object, 'author')
|
||||
)
|
||||
) {
|
||||
console.warn(`${language}/${fileName}.json has malformed quote objects.`);
|
||||
}
|
||||
console.info(`${language} ${fileName}.json validation complete`);
|
||||
};
|
||||
|
||||
const readJsonFile = async (language: string, fileName: string) => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
`/locales/${language}/${fileName}.json`
|
||||
);
|
||||
const file = await readFile(filePath, 'utf8');
|
||||
const fileJson = JSON.parse(file) as Record<string, unknown>;
|
||||
return fileJson;
|
||||
};
|
||||
|
||||
const translatedLangs = availableLangs.client.filter(x => x !== 'english');
|
||||
|
||||
translationSchemaValidation(translatedLangs);
|
||||
trendingSchemaValidation(translatedLangs);
|
||||
motivationSchemaValidation(translatedLangs);
|
||||
introSchemaValidation(translatedLangs);
|
||||
metaTagsSchemaValidation(translatedLangs);
|
||||
linksSchemaValidation(translatedLangs);
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
"clean": "gatsby clean",
|
||||
"predevelop": "npm --prefix ../ run create:config && npm run build:workers -- --env development",
|
||||
"develop": "cross-env NODE_OPTIONS=\"--max-old-space-size=5000\" gatsby develop --inspect=9230",
|
||||
"lint": "node ./i18n/schema-validation.js",
|
||||
"lint": "ts-node ./i18n/schema-validation.ts",
|
||||
"serve": "gatsby serve -p 8000",
|
||||
"serve-ci": "serve -l 8000 -c serve.json public",
|
||||
"prestand-alone": "npm run prebuild",
|
||||
|
||||
Reference in New Issue
Block a user