/**
* 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();