npm package diff

Package: @forge/manifest

Versions: 7.7.0-next.8-experimental-c7a7d36 - 7.7.0-next.11

File: package/out/validators/translations-validator.js

Index: package/out/validators/translations-validator.js
===================================================================
--- package/out/validators/translations-validator.js
+++ package/out/validators/translations-validator.js
@@ -3,16 +3,21 @@
 exports.TranslationsValidator = void 0;
 const tslib_1 = require("tslib");
 const fs_1 = tslib_1.__importDefault(require("fs"));
 const path_1 = require("path");
+const lodash_1 = require("lodash");
+const ajv_1 = tslib_1.__importDefault(require("ajv"));
+const manifest_schema_json_1 = tslib_1.__importDefault(require("../schema/manifest-schema.json"));
 const text_1 = require("../text");
 const text_2 = require("../text");
 const utils_1 = require("../utils");
 class TranslationsValidator {
+    validateCache = new Map();
+    ajv = new ajv_1.default({ allErrors: true, verbose: true, strict: false });
     ensureValidResourcesDefinition(validationErrors, manifest) {
-        const { resources, fallback } = manifest.yamlContent.translations;
+        const { resources } = manifest.yamlContent.translations;
         const resourcesMap = new Map();
-        resources.forEach(({ key, path }) => {
+        for (const { key, path } of resources) {
             if (resourcesMap.has(key)) {
                 validationErrors.push({
                     message: text_1.errors.translations.duplicateResourceKey(key),
                     reference: text_2.References.SchemaError,
@@ -22,18 +27,16 @@
             }
             else {
                 resourcesMap.set(key, path);
             }
-        });
-        let defaultLanguageLookup = {};
-        resourcesMap.forEach((path, key) => {
+        }
+        const allLanguageLookup = {};
+        for (const [key, path] of resourcesMap) {
             const resourcePath = (0, path_1.resolve)(path);
             try {
                 if (fs_1.default.lstatSync(resourcePath).isFile()) {
                     const data = JSON.parse(fs_1.default.readFileSync((0, path_1.resolve)(path), 'utf8'));
-                    if (fallback?.default === key) {
-                        defaultLanguageLookup = data;
-                    }
+                    allLanguageLookup[key] = data;
                 }
             }
             catch (e) {
                 validationErrors.push({
@@ -42,27 +45,27 @@
                     level: 'error',
                     ...(0, utils_1.findPosition)(`path: ${path}`, manifest?.yamlContentByLine)
                 });
             }
-        });
-        return defaultLanguageLookup;
+        }
+        return allLanguageLookup;
     }
     ensureValidFallbackDefinition(validationErrors, manifest) {
         const { resources, fallback } = manifest.yamlContent.translations;
         const defaultLanguage = fallback.default;
         const resourcesSet = new Set(resources.map((resource) => resource.key));
         const fallbackLanguages = Object.keys(fallback).filter((fallbackLanguage) => fallbackLanguage !== 'default');
         const allFallbackLanguagesSet = new Set([defaultLanguage, ...fallbackLanguages]);
-        allFallbackLanguagesSet.forEach((fallbackLanguage) => {
+        for (const fallbackLanguage of allFallbackLanguagesSet) {
             if (!resourcesSet.has(fallbackLanguage)) {
                 validationErrors.push({
                     message: text_1.errors.translations.missingTranslationsJsonFile(fallbackLanguage),
                     reference: text_2.References.SchemaError,
                     level: 'error',
                     ...(0, utils_1.findPosition)(fallbackLanguage === defaultLanguage ? `default: ${fallbackLanguage}` : `${fallbackLanguage}:`, manifest.yamlContentByLine)
                 });
             }
-        });
+        }
         const allLanguagesList = [
             defaultLanguage,
             ...fallbackLanguages,
             ...fallbackLanguages.flatMap((language) => fallback[language])
@@ -70,33 +73,140 @@
         const [, duplicates] = allLanguagesList.reduce(([languageSet, duplicates], language) => {
             languageSet.has(language) ? duplicates.add(language) : languageSet.add(language);
             return [languageSet, duplicates];
         }, [new Set(), new Set()]);
-        duplicates.forEach((duplicate) => {
+        for (const duplicate of duplicates) {
             validationErrors.push({
                 message: text_1.errors.translations.duplicateFallbackConfig(duplicate),
                 reference: text_2.References.SchemaError,
                 level: 'error',
                 ...(0, utils_1.findPosition)(duplicate, manifest.yamlContentByLine)
             });
-        });
+        }
     }
-    ensureI18nKeysExistInDefaultJson(validationErrors, i18nKeys, defaultLanguageLookup, manifest) {
-        const i18nKeysSet = new Set(i18nKeys);
-        const defaultLocalCode = manifest.yamlContent.translations.fallback.default;
-        const languageLookUp = { [defaultLocalCode]: defaultLanguageLookup };
-        i18nKeysSet.forEach((key) => {
-            const i18nValue = (0, utils_1.getTranslationValue)(languageLookUp, key, defaultLocalCode);
+    getAllLocalesLookup(validationErrors, i18nKeys, translationsLookup, manifest) {
+        const { resources, fallback } = manifest.yamlContent.translations;
+        const defaultLocaleCode = fallback.default;
+        return resources
+            .map((resource) => resource.key)
+            .reduce((allLocalesLookup, locale) => {
+            const i18nMap = this.getI18nMap(i18nKeys, translationsLookup, locale);
+            if (locale === defaultLocaleCode) {
+                this.ensureI18nKeysExistInDefaultJson(validationErrors, i18nKeys, translationsLookup, locale, manifest);
+            }
+            allLocalesLookup.set(locale, i18nMap);
+            return allLocalesLookup;
+        }, new Map());
+    }
+    ensureI18nKeysExistInDefaultJson(validationErrors, i18nKeys, translationsLookup, locale, manifest) {
+        for (const i18nKey of i18nKeys) {
+            const i18nValue = (0, utils_1.getTranslationValue)(translationsLookup, i18nKey, locale);
             if (!i18nValue) {
                 validationErrors.push({
-                    message: text_1.errors.translations.i18nKeyNotFound(key),
+                    message: text_1.errors.translations.i18nKeyNotFound(i18nKey),
                     reference: text_2.References.SchemaError,
                     level: 'error',
-                    ...(0, utils_1.findPosition)(`i18n: ${key}`, manifest.yamlContentByLine)
+                    ...(0, utils_1.findPosition)(`i18n: ${i18nKey}`, manifest.yamlContentByLine)
                 });
             }
+        }
+    }
+    getI18nMap(i18nKeys, translationsLookup, locale) {
+        return i18nKeys.reduce((i18nMap, key) => {
+            const i18nValue = (0, utils_1.getTranslationValue)(translationsLookup, key, locale);
+            if (i18nValue) {
+                i18nMap.set(key, i18nValue);
+            }
+            return i18nMap;
+        }, new Map());
+    }
+    getI18nPropertySchema(schemaSlice, i18nPath) {
+        if (typeof schemaSlice !== 'object' || schemaSlice === null) {
+            return [];
+        }
+        const [propertyName, ...restPath] = i18nPath;
+        if (Array.isArray(schemaSlice)) {
+            return schemaSlice.flatMap((object) => {
+                return this.getI18nPropertySchema(object, i18nPath);
+            });
+        }
+        return Object.entries(schemaSlice).flatMap(([key, value]) => {
+            if (key === propertyName) {
+                if (restPath.length === 0 && typeof value === 'object') {
+                    return [value];
+                }
+                else if (restPath.length > 0) {
+                    return this.getI18nPropertySchema(value, restPath);
+                }
+                else {
+                    return [];
+                }
+            }
+            else {
+                return this.getI18nPropertySchema(value, i18nPath);
+            }
         });
     }
+    getValidateI18nFn(i18nPropertySchema) {
+        const schemaKey = JSON.stringify(i18nPropertySchema);
+        if (this.validateCache.has(schemaKey)) {
+            return this.validateCache.get(schemaKey);
+        }
+        const validate = this.ajv.compile(i18nPropertySchema);
+        this.validateCache.set(schemaKey, validate);
+        return validate;
+    }
+    getI18nPropertyValidator({ modulesSchema, i18nPropertyPath, i18nKey, locale, manifest }) {
+        const i18nPropertySchemas = this.getI18nPropertySchema(modulesSchema, i18nPropertyPath);
+        return (i18nValue) => {
+            const validationResults = i18nPropertySchemas.reduce((validationResults, i18nPropertySchema) => {
+                const validate = this.getValidateI18nFn(i18nPropertySchema);
+                if (!validationResults.isValid && !validate(i18nValue)) {
+                    const validationErrors = validate.errors.reduce((validationErrors, validationError) => {
+                        if (validationError.message) {
+                            validationErrors.push({
+                                message: text_1.errors.translations.i18nValueValidationError(i18nKey, locale, validationError.message),
+                                reference: text_2.References.SchemaError,
+                                level: 'error',
+                                ...(0, utils_1.findPosition)(`i18n: ${i18nKey}`, manifest?.yamlContentByLine)
+                            });
+                        }
+                        return validationErrors;
+                    }, []);
+                    validationResults.errors.push(validationErrors);
+                }
+                else {
+                    validationResults.isValid = true;
+                }
+                return validationResults;
+            }, { errors: [], isValid: false });
+            return validationResults.isValid ? [] : validationResults.errors;
+        };
+    }
+    ensureValidI18nValue(validationErrors, i18nMap, moduleI18nProperties, locale, manifest) {
+        for (const i18n of moduleI18nProperties) {
+            const i18nValue = i18nMap.get(i18n.key);
+            if (i18nValue) {
+                const modulesSchema = (0, lodash_1.get)(manifest_schema_json_1.default.definitions.ModuleSchema.properties, i18n.moduleName);
+                const validator = this.getI18nPropertyValidator({
+                    modulesSchema,
+                    i18nPropertyPath: i18n.propertyPath,
+                    i18nKey: i18n.key,
+                    locale,
+                    manifest
+                });
+                const validationResults = validator(i18nValue);
+                if (validationResults.length > 0) {
+                    validationErrors.push(...validationResults[0]);
+                }
+            }
+        }
+    }
+    ensureAllValidI18nValue(validationErrors, allI18nMap, moduleI18nProperties, manifest) {
+        for (const [locale, i18nMap] of allI18nMap) {
+            this.ensureValidI18nValue(validationErrors, i18nMap, moduleI18nProperties, locale, manifest);
+        }
+    }
     validateManifestWithoutI18nConfig(manifest, i18nKeys) {
         if (i18nKeys.length === 0) {
             return {
                 success: true,
@@ -116,13 +226,14 @@
             manifestObject: manifest,
             errors: missingTranslationsPropertyError
         };
     }
-    validateManifestI18nConfig(manifest, i18nKeys) {
+    validateManifestI18nConfig(manifest, i18nKeys, moduleI18nProperties) {
         const validationErrors = [];
-        const defaultLanguageLookup = this.ensureValidResourcesDefinition(validationErrors, manifest);
+        const allLanguageLookup = this.ensureValidResourcesDefinition(validationErrors, manifest);
         this.ensureValidFallbackDefinition(validationErrors, manifest);
-        this.ensureI18nKeysExistInDefaultJson(validationErrors, i18nKeys, defaultLanguageLookup, manifest);
+        const i18nMap = this.getAllLocalesLookup(validationErrors, i18nKeys, allLanguageLookup, manifest);
+        this.ensureAllValidI18nValue(validationErrors, i18nMap, moduleI18nProperties, manifest);
         return validationErrors;
     }
     validateInternalI18nPropertyKeysNotInModules(manifest) {
         const modules = manifest?.typedContent?.modules ?? {};
@@ -140,15 +251,16 @@
                 success: false,
                 manifestObject: manifest
             };
         }
-        const i18nKeys = (0, utils_1.extractI18nKeysFromModules)(manifest?.typedContent?.modules ?? {});
+        const moduleI18nProperties = (0, utils_1.extractI18nPropertiesFromModules)(manifest?.typedContent?.modules ?? {});
+        const i18nKeys = moduleI18nProperties.map((i18nConfig) => i18nConfig.key);
         const i18nConfig = manifest?.yamlContent?.translations;
         if (!i18nConfig) {
             return this.validateManifestWithoutI18nConfig(manifest, i18nKeys);
         }
         const validationErrors = [
-            ...this.validateManifestI18nConfig(manifest, i18nKeys),
+            ...this.validateManifestI18nConfig(manifest, i18nKeys, moduleI18nProperties),
             ...this.validateInternalI18nPropertyKeysNotInModules(manifest)
         ];
         return {
             success: validationErrors.length === 0,