From fccafe2dd8aafe031e69dc6670117691278e0b73 Mon Sep 17 00:00:00 2001 From: Roman Krasinskyi Date: Wed, 22 Jan 2020 13:53:44 +0200 Subject: [PATCH] feat(translate): added validation for schemaTranslations.json --- lib/bundle-validator.js | 114 +++++++++++++++++++++++++++++ lib/schemaTranslations.schema.json | 22 ++++++ lib/stencil-bundle.js | 20 +++++ lib/theme-config.js | 26 +++++++ 4 files changed, 182 insertions(+) create mode 100644 lib/schemaTranslations.schema.json diff --git a/lib/bundle-validator.js b/lib/bundle-validator.js index 3b2d54d7..154b0b35 100644 --- a/lib/bundle-validator.js +++ b/lib/bundle-validator.js @@ -34,6 +34,7 @@ function BundleValidator(themePath, themeConfig, isPrivate) { // Array of tasks used in Async.series this.validationTasks = [ validateThemeConfiguration.bind(this), + validateSchemaTranslations.bind(this), validateJspmSettings.bind(this), ]; @@ -101,6 +102,119 @@ function validateThemeConfiguration(callback) { callback(null, true); } +/** + * Find translatable strings + * @param {object} schema + * @param {function} callback + */ +function getTranslatableStrings(schema, callback) { + var trackedKeys = ['name', 'content', 'label', 'settings', 'options']; + + schema.forEach(element => { + Object.keys(element).forEach(key => { + const value = element[key]; + + if (trackedKeys.indexOf(key) === -1) { + return; + } + + if (Array.isArray(value)) { + return getTranslatableStrings(value, callback); + } + + callback(value); + }); + }); +} + +/** + * Find i18n keys in schema + * @param {object} schema + * @return {array} + */ +function getI18nKeys(schema) { + var keys = []; + + getTranslatableStrings(schema, (value) => { + if ( + value + && keys.indexOf(value) === -1 + && /^i18n\./.test(value) + ) { + keys.push(value); + } + }); + + return keys; +} + +/** + * Find missed i18n keys in schemaTranslations + * @param {array} keys + * @param {object} translations + * @return {array} + */ +function findMissedKeys(keys, translations) { + var translationsKeys = Object.keys(translations); + var missingKeys = []; + + keys.forEach(key => { + if (translationsKeys.indexOf(key) === -1) { + missingKeys.push(key); + } + }); + + return missingKeys; +} + +/** + * Ensure that schema translations exists and there are no missing keys. + * @param {function} callback + * @return {function} callback + */ +function validateSchemaTranslations(callback) { + var v = new Validator(); + var validatorTranslations = './schemaTranslations.schema.json'; + var translations = {}; + var keys = []; + var missedKeys; + var validation; + var errorMessage; + + if (this.themeConfig.schemaExists()) { + keys = getI18nKeys(this.themeConfig.getRawSchema()); + } + + if (this.themeConfig.schemaTranslationsExists()) { + translations = this.themeConfig.getRawSchemaTranslations(); + } + + if (keys.length && _.isEmpty(translations)) { + errorMessage = 'Missed or corrupted schemaTranslations.json file'.red; + + return callback(new Error(errorMessage)); + } + + missedKeys = findMissedKeys(keys, translations); + validation = v.validate(translations, require(validatorTranslations)); + + if ((validation.errors && validation.errors.length > 0) || (missedKeys && missedKeys.length > 0)) { + errorMessage = 'Your theme\'s schemaTranslations.json has errors:'.red; + + missedKeys.forEach(key => { + errorMessage += '\r\nmissing translation key "'.red + key.red + '"'.red; + }); + + validation.errors.forEach(error => { + errorMessage += '\r\nschemaTranslations'.red + error.stack.substring(8).red; + }); + + return callback(new Error(errorMessage)); + } + + callback(null, true); +} + /** * If a theme is using JSPM, make sure they * @param callback diff --git a/lib/schemaTranslations.schema.json b/lib/schemaTranslations.schema.json new file mode 100644 index 00000000..0b07c21f --- /dev/null +++ b/lib/schemaTranslations.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "type": "object", + "patternProperties": { + "^i18n.": { + "type": "object", + "properties": { + "default": { + "type": "string" + } + }, + "additionalProperties": { + "type": "string" + }, + "required": [ + "default" + ] + } + }, + "additionalProperties": false +} diff --git a/lib/stencil-bundle.js b/lib/stencil-bundle.js index c8ddd5c1..e05cc203 100644 --- a/lib/stencil-bundle.js +++ b/lib/stencil-bundle.js @@ -57,6 +57,7 @@ function Bundle(themePath, themeConfig, rawConfig, options) { tasks.templates = this.assembleTemplatesTask.bind(this); tasks.lang = this.assembleLangTask.bind(this); tasks.schema = this.assembleSchema.bind(this); + tasks.schemaTranslations = this.assembleSchemaTranslations.bind(this); if (typeof buildConfig.production === 'function') { tasks.theme = callback => { @@ -184,6 +185,20 @@ Bundle.prototype.assembleSchema = function (callback) { }); }; +Bundle.prototype.assembleSchemaTranslations = function (callback) { + console.log('Schema Translations Parsing Started...'); + + this.themeConfig.getSchemaTranslations((err, schema) => { + if (err) { + callback(err); + } + + console.log('ok'.green + ' -- Schema Translations Parsing Finished'); + + callback(null, schema); + }); +}; + Bundle.prototype.assembleLangTask = function (callback) { console.log('Language Files Parsing Started...'); LangAssembler.assemble((err, results) => { @@ -398,6 +413,11 @@ function bundleParsedFiles(archive, taskResults) { archiveJsonFile(data, 'schema.json'); break; + case 'schemaTranslations': + // append the parsed schemaTranslations.json file + archiveJsonFile(data, 'schemaTranslations.json'); + break; + case 'manifest': // append the generated manifest.json file archiveJsonFile(data, 'manifest.json'); diff --git a/lib/theme-config.js b/lib/theme-config.js index 860c1890..b3f61ad9 100644 --- a/lib/theme-config.js +++ b/lib/theme-config.js @@ -291,6 +291,14 @@ ThemeConfig.prototype.schemaExists = function () { return fileExists(this.schemaPath); }; +/** + * Check if the schemaTranslations.json file exists + * @return {Boolean} + */ +ThemeConfig.prototype.schemaTranslationsExists = function () { + return fileExists(this.schemaTranslationsPath); +}; + /** * Scans the theme template directory for theme settings that need force reload * @@ -410,6 +418,24 @@ ThemeConfig.prototype.getRawConfig = function() { return jsonLint.parse(Fs.readFileSync(this.configPath, {encoding: 'utf-8'}), this.configPath); }; +/** + * Return the raw schema.json data + * + * @return {object} + */ +ThemeConfig.prototype.getRawSchema = function() { + return jsonLint.parse(Fs.readFileSync(this.schemaPath, {encoding: 'utf-8'}), this.schemaPath); +}; + +/** + * Return the raw config.json data + * + * @return {object} + */ +ThemeConfig.prototype.getRawSchemaTranslations = function() { + return jsonLint.parse(Fs.readFileSync(this.schemaTranslationsPath, {encoding: 'utf-8'}), this.schemaTranslationsPath); +}; + /** * Grabs out a variation based on a name. Or if the name is not passed in, the very first one in the list. *