Source: job-manager/translationFilter.js

Source: job-manager/translationFilter.js

/**
 * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
 * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
 */
/* jshint esversion: 6 */

var filterProperty = function (path, filter) {
	var includeProperty = true,
		lowerCasePath = path.toLowerCase(),
		includeFilters = filter && filter.include,
		excludeFilters = filter && filter.exclude,
		includeStandardName = filter && filter.includeStandardName,
		includeStandardDesc = filter && filter.includeStandardDesc,
		findFunc = function (regEx) {
			return regEx.test(lowerCasePath);
		};

	if (filter === undefined) {
		return true;
	}

	if (findFunc(/^name$/)) {
		return includeStandardName;
	}

	if (findFunc(/^description$/)) {
		return includeStandardDesc;
	}

	// check whether to include the property
	if (includeProperty && includeFilters) {
		includeProperty = !!includeFilters.find(findFunc);
	}

	// check whether to exclude the property
	if (includeProperty && excludeFilters) {
		includeProperty = !excludeFilters.find(findFunc);
	}

	return includeProperty;
};

// custom constraint to restrict the length of a field 
var restrictLength = function (value, args) {
	var length = args && args.length,
		updatedValue = value;

	// restrict the length to the required amount
	if (length !== null && !isNaN(length) && updatedValue) {
		updatedValue = updatedValue.substr(0, length);
	}
	return updatedValue;
};

var filterConstraints = function (path, value, constraints) {
	var updatedValue = value,
		lowerCasePath = path.toLowerCase();

	// apply any custom constraints
	if (constraints) {
		Object.keys(constraints).forEach(function (key) {
			// see if we should apply this constraint to this value
			if (constraints[key].match.test(lowerCasePath)) {
				var constraint = constraints[key].constraint; 
				// apply all the constraints for this match
				if (constraint && typeof constraint.func === 'function') {
					updatedValue = constraint.func(updatedValue, constraint.args);
				}
			}
		});
	}

	return updatedValue;
};

/**
 * A filter to extract translatable strings from OCE assets and re-combine the assets with the translated strings.
 * @constructor
 * @alias TranslationFilter
 */
var TranslationFilter = function () {};

/**
 * Do a deep merge of the properties in the source object into the destination object.
 * @param {object} args The arguments used by the "mergeProperties" function.
 * @param {object} args.source The source JSON object to copy the properties from.
 * @param {object} args.dest The destination JSON object copy the properties into.
 * @param {string} [args.path] A string representing the property path to the current object in the source file. For example: "fields.title". On the first call, this is not supplied and is built up during the copy recursion.
 * @param {RegEx[]} [args.filter] Any regular expression filters that should be applied to the path when copying the properties. If not supplied, all properties are copied.
 */
TranslationFilter.prototype.mergeProperties = function (args) {
	var self = this;

	// extract the parameters
	var dest = args.dest,
		source = args.source,
		filter = args.filter,
		constraints = args.constraints,
		path = args.path;

	Object.keys(source).forEach(function (key) {
		// get the path followed to this key in the object
		var keyPath = (path ? path + '.' : '') + key;

		// don't merge empty objects [null | undefined | empty strings] and make sure property is not filtered out
		if ((source[key] || source[key] === 0) && filterProperty(keyPath, filter) && typeof source[key] !== 'boolean') {
			// apply any constraint to the value before merging it
			source[key] = filterConstraints(keyPath, source[key], constraints);

			if (Array.isArray(source[key])) {
				// Handle merging of each item in the array
				// Note: order of items in the array must remain unchanged during translation
				dest[key] = dest[key] || [];
				var destArray = dest[key];

				for (var i = 0; i < source[key].length; i++) {
					var sourceValue = source[key][i];

					if (sourceValue !== null && typeof sourceValue === 'object') {
						// deep merge objects
						if (destArray.length <= i) {
							destArray.push({});
						}
						self.mergeProperties({
							dest: destArray[i],
							source: sourceValue,
							filter: filter,
							path: keyPath
						});
					} else {
						// copy across scalars
						// Note: we include "null" values in here to maintain order of array
						if (destArray.length <= i) {
							destArray.push(sourceValue);
						} else {
							destArray[i] = sourceValue;
						}
					}
				}
			} else if (typeof source[key] === 'object') {
				// deep merge objects
				dest[key] = dest[key] || {};
				self.mergeProperties({
					dest: dest[key],
					source: source[key],
					filter: filter,
					path: keyPath
				});
			} else {
				// merge scalars
				dest[key] = source[key];
			}
		}
	});
};


/**
 * Filter out the translatable fields from the document<br/>
 * Provide a set of "include" and "exclude" filters for each asset type. </br>
 * These regexes will be applied to the complete property path converted to lower case.<br/>
 * For example, an asset has: <br>
 * <pre>
 * {
 *   fields: {
 *         Category: 'one',
 *         Title: 'a simple example'
 *     }
 * }
 * </pre>
 * Filtering is applied against the strings: 
 * <ul>
 *  <li>"fields"</li>
 *  <li>"fields.category" </li>
 *  <li>"fields.title"</li>
 * </ul>
 */
TranslationFilter.prototype.filters = {
	assets: {
		includeStandardName: false,
		includeStandardDesc: false,
		include: [
			/^fields$/
		],
		exclude: [
			/^fields\..*\.id$/, // "fields.*.id" - referenced content
			/^fields\..*\.type$/, // "fields.*.type" - referenced content
			/^fields\..*\.typecategory$/, // "fields.*.typecategory" - referenced content
			/^fields\..*_dnt$/ // exclude any fields property that end in "_dnt" (Do Not Translate)
		]
	},
	site: {
		includeStandardName: false,
		includeStandardDesc: false,
		exclude: [
			/^id$/,
			/\.id$/
		]
	}
};
/**
 * Apply constraints to the translation results <br/>
 * Make sure that the translation results will be valid.  For example, the "name" attribute of an asset cannot be greater than 64 characters. 
 * These regexes will be applied to the complete property path converted to lower case.<br/>
 * For example, an asset has: <br>
 * <pre>
 * {
 *   fields: {
 *         Category: 'one',
 *         Title: 'a simple example'
 *     }
 * }
 * </pre>
 * Constraints are applied against the strings: 
 * <ul>
 *  <li>"fields"</li>
 *  <li>"fields.category" </li>
 *  <li>"fields.title"</li>
 * </ul>
 */
TranslationFilter.prototype.constraints = {
	assets: {
		"nameLength": {
			description: "Restrict 'name' system field translation to 64 characters",
			match: /^name$/,
			constraint: {
				func: restrictLength,
				args: {
					length: 64
				}
			}
		}
	},
	site: {}
};

/**
 * Get the translatable strings from the base document.
 * 
 * @param {object} document The JSON document to apply the filters to.
 * @param {("assets" | "site")} filterType the filter type from {@link TranslationFilter.filters} to apply to the document.
 * @param {object} includes Array of fields to include by field name.
 * @returns {object} A new JSON object that contains only translatable strings from the given document.
 */
TranslationFilter.prototype.getTranslatableProperties = function (document, filterType, includes) {
	// start with an empty document
	var filteredDoc = {};

	// Deep copy of array elements
	var filters = Object.assign({}, this.filters[filterType]);
	var requestFilters = {};

	Object.keys(filters).forEach(function(key) {
		if (Array.isArray(filters[key])) {
			requestFilters[key] = filters[key].slice(0);
		} else {
			requestFilters[key] = filters[key];
		}
	});

	if (typeof includes !== 'undefined' && includes.length > 0) {
		// includes contain field names for translation
		// Create a regular expression for each field name in includes.
		includes.forEach(function(e) {
			if (e === 'name') {
				requestFilters.includeStandardName = true;
			} else  if (e === 'description') {
				requestFilters.includeStandardDesc = true;
			} else {
				// Expect field name to follow fields immediately.
				// E.g. fields.field1
				requestFilters.include.push(new RegExp('^fields\\\.' + e + '$'));
			}
		});
	}

	// do a deep merge of the base documents filtered properties onto an empty document 
	this.mergeProperties({
		dest: filteredDoc,
		source: typeof document === 'string' ? JSON.parse(document) : JSON.parse(JSON.stringify(document)),
		filter: requestFilters
	});

	// return the filtered properties
	return filteredDoc;
};

/**
 * Re-combine the translated filtered fields with the original document.
 * 
 * @param {object} document The JSON document to apply the filters to.
 * @param {object} translatedDocument The JSON document containing the translated string
 * @param {("assets" | "site")} constraintType the constraint type from {@link TranslationFilter.constraints} to apply to the document.
 * @returns {object} A JSON object which is a copy of the base document with the translated strings applied to it.
 */
TranslationFilter.prototype.applyTranslation = function (document, translatedDocument, constraintType) {
	// copy the original document
	var mergedDoc = JSON.parse(JSON.stringify(document));

	// do a deep merge of the translation document onto the copy
	// Note: 
	//  No filters are applied so all translated properties are included
	//  However, we apply constraints to the translation results to ensure they can be imported albeit with updates
	this.mergeProperties({
		dest: mergedDoc,
		source: translatedDocument,
		constraints: constraintType ? this.constraints[constraintType] : undefined
	});

	// return the merged document
	return mergedDoc;
};

// Export the mock translator
module.exports = new TranslationFilter();