(function() {
/**
* @typedef {Object} Help4.engine.MLTEngine.UrlConfig
* @property {string} serverBaseUrl - base URL of the content server
* @property {string} translation - URL extension for translation calls
* @property {string} feature - URL extension to check if feature is supported in SEN
* @property {string} permission - URL extension to check if user has permission in SEN
* @property {string} supportedLanguages - URL extension to get supported languages
*/
/**
* @typedef {Object} Help4.engine.MLTEngine.ContentObject
* @property {string} data
* @property {string} contentType
* @property {string} language
*/
/**
* @typedef {Object} Help4.engine.MLTEngine.TranslationData
* @property {Object} data - {en-US: Help4.engine.MLTEngine.ContentObject[], ...}
* @property {Object} meta - {en-US: [{original: <string>, id: <string>, widgetName: <string>, subName?: <string>}], de-DE: [{}]}
* @property {Object} cache - {en-US: [{translation: <string>, original: <string>, id: <string>, widgetName: <string>, subName?: <string>}], de-DE: [{}]}
*/
/**
* @typedef {
* Help4.widget.help.view2.GlobalHelpDialogControl|
* Help4.widget.help.view2.BubbleControl|
* Help4.widget.tour.BubbleControl
* } Help4.engine.MLTEngine.Bubbles
*/
/**
* @typedef {Help4.jscore.ControlBase.Params} Help4.engine.MLTEngine.Params
* @property {Help4.controller.Controller} controller
* @property {Help4.engine.DomRefreshEngine} domRefreshEngine
*/
/**
* @typedef {Object} Help4.engine.MLTEngine.SupportedFeaturesResponse
* @property {Object} response
* @property {Object} response.feature
* @property {Array<string>} response.feature.featureNames
*/
/**
* @typedef {Object} Help4.engine.MLTEngine.PermissionsResponse
* @property {Object} response
* @property {Object} response.permission
* @property {Array<string>} response.permission.permissionNames
*/
/**
* @typedef {Object} Help4.engine.MLTEngine.ServerResponseContent
* @property {string} contentType - content type of the text
* @property {string} data - translated text
* @property {string} encoding
* @property {number} status - status code of the translation e.g. 200, 500 etc.
*/
/**
* @typedef {Object} Help4.engine.MLTEngine.ServerResponse
* @property {Help4.engine.MLTEngine.ServerResponseContent} content
* @property {string} sourceLanguage
* @property {string} targetLanguage
*/
const CACHE_EXEMPTIONS = ['content-caption.text'];
/**
* MLTEngine class translates content on-the-fly
* @augments Help4.jscore.ControlBase
* @property {Help4.controller.Controller} controller
* @property {Help4.engine.DomRefreshEngine} domRefreshEngine
* @property {boolean} _started
* @property {Function} _domRefreshExecutor
* @property {Function} _onPanelEvent
* @property {string} _targetLanguage
* @property {string[]} _sourceLanguages
* @property {string} _preferredLanguage
* @property {Object} _languageMap - {en-US: ['de-DE', 'es-ES'], de-DE: ['en-US', 'es-ES'], ...}
* @property {boolean} _translationActive
* @property {Help4.engine.MLTEngine.Bubbles[]} _openBubbles
* @property {Help4.engine.MLTEngine.Bubbles} _activeBubble
* @property {Help4.engine.MLTEngine.Bubbles} _translatedBubble
* @property {string|null} _excludeBubble
* @property {boolean} _translationAvailable
* @property {Object} _cache - {en: {id1: {translation: <string>, original: <string>}, id2: {}}, de: {id3: {...}, id4: {...}}}
* @property {Object} _xhrPromiseContainer
* @property {boolean} _xhrError
*/
Help4.engine.MLTEngine = class extends Help4.jscore.ControlBase {
/**
* @override
* @param {Help4.engine.MLTEngine.Params} params
*/
constructor(params) {
const domRefreshExecutor = async () => {
this._languageSelectionDialog?.align();
_translate.call(this);
}
const onPanelEvent = async () => await _startStopTranslation.call(this);
const {TYPES: T} = Help4.jscore.ControlBase;
super(params, {
params: {
controller: {type: T.instance, mandatory: true, readonly: true},
domRefreshEngine: {type: T.instance, mandatory: true, readonly: true}
},
statics: {
_started: {init: false, destroy: false},
_domRefreshExecutor: {init: domRefreshExecutor, destroy: false},
_onPanelEvent: {init: onPanelEvent, destroy: false},
_targetLanguage: {init: null, destroy: false},
_sourceLanguages: {init: [], destroy: false},
_preferredLanguage: {init: null, destroy: false},
_languageMap: {init: {}, destroy: false},
_translationActive: {init: false, destroy: false},
_openBubbles: {init: [], destroy: false},
_activeBubble: {init: null, destroy: false},
_translatedBubble: {init: null, destroy: false},
_excludeBubble: {init: null, destroy: false},
_translationAvailable: {init: false, destroy: false},
_cache: {init: {}, destroy: false},
_languageSelectionDialog: {init: null},
_xhrPromiseContainer: {init: null, destroy: false},
_xhrError: {init: false, destroy: false}
}
});
}
/**
* @type {Object}
* @property {'success'} success
* @property {'fail'} fail
* @property {'partial'} partial
* @property {'skipped'} skipped
*/
static TRANSLATION_STATES = {
success: 'success',
fail: 'fail',
partial: 'partial',
skipped: 'skipped'
}
/** @override */
destroy() {
this.stop();
super.destroy();
}
/** @returns {Promise<void>} */
async start() {
if (this._started) return;
// XRAY-6404
const {controller} = this;
const panel = controller?.getPanel();
const {STATUS: {done}} = Help4.StartStatus;
const statusService = Help4.getController()?.getService('startStatus');
if (!panel || !statusService.has(done)) return void setTimeout(() => this.start(), 100);
if (this.isDestroyed() || !(await _checkPreRequirements.call(this))) return;
this._started = true;
const {
domRefreshEngine,
_domRefreshExecutor,
_onPanelEvent,
_languageSelectionDialog
} = this;
_languageSelectionDialog && _closeLanguageSelectionDialog.call(this);
domRefreshEngine.addExecutor(_domRefreshExecutor);
panel.addListener('translate', _onPanelEvent);
const config = /** @type {Help4.typedef.SystemConfiguration} */ controller.getConfiguration();
const {translation: {continuousMLT}} = config;
const {mltPreferredLanguage, mltTranslationActive, mltTargetLanguage} = controller.getCmp4Handler();
this._preferredLanguage = mltPreferredLanguage;
if (continuousMLT && mltTranslationActive) {
this._targetLanguage = mltTargetLanguage;
await _startStopTranslation.call(this, {active: true, force: true});
}
await _translate.call(this);
}
/** @returns {Promise<void>} */
async stop() {
if (!this._started) return;
this._started = false;
const {
domRefreshEngine,
_domRefreshExecutor,
controller,
_onPanelEvent,
_openBubbles
} = this;
domRefreshEngine.removeExecutor(_domRefreshExecutor);
const panel = controller.getPanel();
if (panel) {
panel.removeListener('translation', _onPanelEvent);
panel.translation = false;
panel.animateTranslationButton = false;
panel.showTranslationButton = false;
}
for (const bubble of _openBubbles) {
bubble.enableTranslation = false;
bubble.activeTranslation = false;
}
this._translationActive = false;
this._translationAvailable = false;
_cleanXhrPromise.call(this, true);
await _revertTranslation.call(this);
}
/** @returns {boolean} */
isStarted() {
return this._started;
}
/** @returns {boolean} */
isTranslationActive() {
return this._translationActive;
}
/** @returns {string} */
getTargetLanguage() {
return this._targetLanguage;
}
/** @returns {boolean} */
isTranslationAvailable() {
return this._translationAvailable;
}
/** @returns {string} */
getPreferredLanguage() {
return this._preferredLanguage;
}
/** @param {Help4.engine.MLTEngine.Bubbles} bubble */
async registerBubble(bubble) {
if (!this.isStarted()) return;
// should also work if engine is not started
const {_openBubbles} = this;
if (_openBubbles.indexOf(bubble) < 0) _openBubbles.push(bubble);
await _checkAvailableTargetLanguages.call(this);
_setButtonVisibilities.call(this);
}
/** @param {Help4.engine.MLTEngine.Bubbles} bubble */
unregisterBubble(bubble) {
if (!this.isStarted()) return;
// should also work if engine is not started
const {_openBubbles} = this;
const index = _openBubbles.indexOf(bubble);
if (index >= 0) _openBubbles.splice(index, 1);
this._activeBubble = null;
this._excludeBubble = null;
const {_translatedBubble} = this;
if (_translatedBubble === bubble) this._translatedBubble = null;
}
/**
* @param {Object} params
* @param {Help4.engine.MLTEngine.Bubbles} params.bubble
* @param {boolean} params.active
* @throws {Error}
* @return {Promise<void>}
*/
async onBubbleClick({bubble, active}) {
if (!this.isStarted()) return;
const {_openBubbles} = this;
const index = _openBubbles.indexOf(bubble);
if (index < 0) throw new Error('MLT Engine: Unable to handle click for unregistered bubble!');
await _startStopTranslation.call(this, {bubble, active});
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {Object} [params = {}]
* @param {Help4.engine.MLTEngine.Bubbles} [params.bubble]
* @param {boolean} [params.active]
* @param {boolean} [params.force]
* @returns {Promise<void>}
*/
async function _startStopTranslation({bubble, active, force} = {}) {
if (this._xhrPromiseContainer) _cleanXhrPromise.call(this, true);
const {_translationActive} = this;
if (bubble) {
// activated from bubble, translate only bubble
// only open language selection dialog if _targetLanguage is not set or not supported
this._translatedBubble = bubble;
if (!_translationActive && active && await _checkBubbleLanguageMismatch.call(this) && !(await _openLanguageSelectionDialog.call(this))) return; // dialog canceled, return
this._activeBubble = bubble;
this._excludeBubble = active ? null : bubble.id;
_setButtonStates.call(this, active, bubble);
} else {
// activated from panel, translate all
// XRAY-6432: revert bubble translation before activating panel translation
const {_translatedBubble} = this;
if (_translatedBubble) await _startStopTranslation.call(this, {bubble: _translatedBubble, active: false});
// always open LanguageSelectionDialog when mlt is activated from panel
if (!force && !_translationActive && !(await _openLanguageSelectionDialog.call(this))) return; // dialog canceled, return
this._translationActive = active = !_translationActive;
this._excludeBubble = null;
_setButtonStates.call(this, active);
}
active
? await _translate.call(this, !!bubble)
: await _revertTranslation.call(this);
if (!active) this._translatedBubble = null;
this._activeBubble = null;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<boolean>}
*/
async function _checkBubbleLanguageMismatch() {
const {_targetLanguage, _languageMap, controller} = this;
const config = /** @type {Help4.typedef.SystemConfiguration} */ controller.getConfiguration();
const {translation: {continuousMLT}} = config;
// if no targetLanguage force to open dialog
if (!_targetLanguage) return true;
if (continuousMLT) return false;
const {_translatedBubble, _openBubbles} = this;
let openBubbles = _translatedBubble ? [_translatedBubble] : _openBubbles;
// if at least one is supported, no mismatch
for (const bubble of openBubbles) {
const bubbleText = await bubble.getTexts();
if (Object.values(bubbleText).some(({language}) => _languageMap[language].includes(_targetLanguage))) return false;
}
return true;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<boolean>}
*/
function _openLanguageSelectionDialog() {
const {Localization} = Help4;
const {controller, _preferredLanguage} = this;
const dom = controller.getDom2('dom');
const languages = _buildLanguageSelectionData.call(this);
const {core: {language: {uacp}}} = /** @type {Help4.typedef.SystemConfiguration} */ controller.getConfiguration();
// 1. check if preferredLang is selectable - this is cached so not be available after page refresh with different content
// 2. check if only one language is selectable - if yes, preselect
// 3. check if system language is selectable
const preferredLang = languages.find(lang => lang.id === _preferredLanguage)?.id;
const onlyLang = languages.length === 1 && languages[0].id;
const systemLang = languages.find(lang => lang.id === uacp)?.id;
const value = preferredLang || onlyLang || systemLang;
return new Help4.Promise(resolve => {
if (this._languageSelectionDialog) return void resolve(false);
const dialog = this._languageSelectionDialog = new Help4.control2.bubble.LanguageSelection({
caption: Localization.getText('label.languagedialogcaption'),
dom,
languages,
value
})
.addListener('close', () => {
_closeLanguageSelectionDialog.call(this);
resolve(false);
})
.addListener('apply', async ({value}) => {
if (value) {
this._preferredLanguage = this._targetLanguage = value;
}
_closeLanguageSelectionDialog.call(this);
resolve(!!value);
});
dialog.align();
});
}
function _closeLanguageSelectionDialog() {
const {_languageSelectionDialog} = this;
_languageSelectionDialog?.destroy();
this._languageSelectionDialog = null;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Help4.control2.bubble.content.Selection.Data[]}
*/
function _buildLanguageSelectionData() {
const {Localization} = Help4;
const {_languageMap, _translatedBubble, _sourceLanguages} = this;
const data = [];
let sourceLanguages = _sourceLanguages;
// bubble triggered, only show languages from bubble
if (_translatedBubble) {
const bubbleText = _translatedBubble.getTexts();
sourceLanguages = Object.values(bubbleText).map(({language}) => language);
}
for (const sourceLanguage of sourceLanguages) {
const languages = _languageMap[sourceLanguage];
if (!languages) continue;
data.push(...languages.filter(lang => !data.some(d => d.id === lang)).map(lang => ({
id: lang,
text: Localization.getText(`label.mlt.${lang}`) || lang
})));
}
return data.sort((a, b) => a.text.localeCompare(b.text));
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {boolean} active
* @param {Help4.engine.MLTEngine.Bubbles} [bubble = null]
*/
function _setButtonStates(active, bubble = null) {
if (bubble) {
// the translation button in a bubble was clicked
bubble.activeTranslation = active;
} else {
const panel = this.controller.getPanel();
// the translation button in panel was clicked
panel.translation = active;
for (const bubble of this._openBubbles) {
// XRAY-6268: UR bubbles are ignored
if (bubble.isDestroyed() || bubble.getMetadata('isUR')) continue;
bubble.activeTranslation = active;
}
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<void>}
*/
async function _checkAvailableTargetLanguages() {
const texts = await Help4.widget.getTexts();
let sourceLanguage = [];
const getLanguage = data => {
for (const content of Object.values(data)) {
if (!content) continue;
if (_isAtomic.call(this, content)) {
const {language} = content;
if (sourceLanguage.indexOf(language) < 0) sourceLanguage.push(language);
} else {
getLanguage(content);
}
}
}
getLanguage(texts);
this._sourceLanguages = sourceLanguage;
// set _sourceLanguages first, then filter out the ones we have in the map to avoid unnecessary call
const {_languageMap} = this;
sourceLanguage = sourceLanguage.filter(lang => !_languageMap[lang]);
if (!sourceLanguage.length) return void _checkLanguageMismatch.call(this);
// new languages are detected, hide buttons until we get the supported languages
_setButtonVisibilities.call(this, false);
const {
widget: {companionCore: {SEN}},
ajax: {Ajax}
} = Help4;
const {serverBaseUrl, supportedLanguages} = _getUrls.call(this);
try {
const result = await Ajax({
url: serverBaseUrl + supportedLanguages,
saml: true,
xcsrf: SEN.getXCSRF(serverBaseUrl),
method: 'POST',
promise: true,
data: {sourceLanguage}
});
const {response} = result;
for (const [language, targetLanguages] of Object.entries(response)) {
this._languageMap[language] = Help4.isArray(targetLanguages)
? targetLanguages
: []; // ignore the "Unsupported Language" reply to avoid later crashes; XRAY-6451
}
_setButtonVisibilities.call(this);
_checkLanguageMismatch.call(this);
} catch (e) {
const {error, status} = e;
if (error === 'error' && status) {
const {
control2: {InfoBar: {TYPES: {error}}},
Localization
} = Help4;
const {controller} = this;
controller.getService('infobar4').add({
type: error,
content: Localization.getText('label.translationerror')
});
this.stop();
}
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
*/
function _checkLanguageMismatch() {
// check if previous _sourceLanguage is supported (can happen in multi-language scenario)
// if not, reset _sourceLanguage and reset button status
const {_languageMap, _targetLanguage, _sourceLanguages, _translationActive, controller} = this;
const config = /** @type {Help4.typedef.SystemConfiguration} */ controller.getConfiguration();
const {translation: {continuousMLT}} = config;
if (continuousMLT || !_translationActive || !_targetLanguage || !_sourceLanguages.length) return;
for (const sourceLanguage of Object.values(_sourceLanguages)) {
if (_languageMap[sourceLanguage].some(lang => lang === _targetLanguage)) return;
}
this._translationActive = false;
this._targetLanguage = null;
_setButtonStates.call(this, false);
const infobar = controller.getService('infobar4');
const {Localization} = Help4;
infobar.add({content: Localization.getText('label.translationmismatch')});
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {?boolean} visibility
*/
function _setButtonVisibilities(visibility) {
const {controller, _sourceLanguages, _languageMap, _openBubbles} = this;
const translationAvailable = this._translationAvailable = visibility ?? _sourceLanguages.some(lang => _languageMap[lang]);
const panel = controller.getPanel();
panel.showTranslationButton = translationAvailable;
for (const bubble of _openBubbles) {
// XRAY-6268: UR bubbles are ignored
if (bubble.isDestroyed() || bubble.getMetadata('isUR')) continue;
bubble.enableTranslation = translationAvailable;
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {boolean} [isBubble = false] - is it triggered from a bubble
* @returns {Promise<void>}
*/
async function _translate(isBubble = false) {
await _checkAvailableTargetLanguages.call(this);
// if bubble turned off the translation, collect it data for state (_getTranslationData) but exclude it from translation (_getTranslations)
const data = /** @type {?Help4.engine.MLTEngine.TranslationData} */ await _getTranslationData.call(this);
if (data && !this.isDestroyed() && (this._translationActive || isBubble)) {
const texts = await _getTranslations.call(this, data);
if (!texts || this.isDestroyed()) return;
await _setTextsToWidgets.call(this, texts);
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<void>}
*/
async function _revertTranslation() {
const {cache = {}} = /** @type {?Help4.engine.MLTEngine.TranslationData} */ await _getTranslationData.call(this) || {};
if (this.isDestroyed()) return;
const texts = {};
for (const langCache of Object.values(cache)) {
for (const {path, original} of Object.values(langCache)) {
_addToTexts(path, original, texts);
}
}
await _setTextsToWidgets.call(this, texts);
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {Help4.engine.MLTEngine.ContentObject} content
* @returns {boolean}
*/
function _isAtomic({data, contentType, language}) {
return !!data && !!contentType && !!language;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<Help4.engine.MLTEngine.TranslationData|null>}
*/
async function _getTranslationData() {
const {_targetLanguage} = this;
/**
* @param {Object} data
* @param {string[]} ids
* @param {Help4.engine.MLTEngine.TranslationData} [result = {data: {}, meta: {}, cache: {}}]
* @returns {Help4.engine.MLTEngine.TranslationData}
*/
const process = (data, ids = [], result = {data: {}, meta: {}, cache: {}}) => {
for (const [id, content] of Object.entries(data)) {
if (!content) continue;
const path = [...ids, id];
_isAtomic.call(this, content)
? addToData(content, path, result)
: process(content, path, result);
}
return result;
}
/**
* @param {Help4.engine.MLTEngine.ContentObject} content
* @param {string[]} path
* @param {Object} result
* @param {Object} result.data
* @param {Object} result.meta
* @param {Object} result.cache
*/
const addToData = (content, path, {data, meta, cache}) => {
const {language, data: text} = content;
const id = path[path.length - 1];
if (!language || !text || _targetLanguage === language || _isURText.call(this, id)) return;
const {translation, original} = _getFromCache.call(this, language, id, text);
if (translation && original) {
// found in cache, use it both for translate from cache and for revert original
cache[language] ||= [];
cache[language].push({path, translation, original});
} else {
data[language] ||= [];
meta[language] ||= [];
data[language].push(content);
meta[language].push({path: path, text});
}
};
const {_activeBubble} = this;
const texts = _activeBubble
? await _activeBubble.getTexts()
: await Help4.widget.getTexts();
if (this.isDestroyed()) return null;
return process(texts);
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {string} id
* @returns {boolean}
*/
function _isURText(id) {
const instance = /** @type {?Help4.widget.Widget} */ Help4.widget.getActiveInstance();
const name = instance?.getName();
if (name !== 'help') return false;
const {controller} = this;
const panelControls = controller.getPanel()?.getHostedControls();
const tiles = instance.getContext().widget.help.view.getTiles() || [];
const isURTile = tiles.some(({id: tileId, _isUR}) => {
if (!_isUR) return false;
const controlId = panelControls.find(control => control.getMetadata('tileId') === tileId)?.id;
return id.includes(controlId);
});
if (isURTile) return true;
const {_openBubbles = []} = this;
return !!_openBubbles.some(bubble => !bubble.isDestroyed() && bubble.getMetadata('isUR') && id.includes(bubble.id));
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {Help4.engine.MLTEngine.TranslationData} translationData
*/
function _excludeBubbleData({data, meta, cache}) {
const {_excludeBubble} = this;
/**
* @param {Object} values
* @param {boolean} [deleteFromData = true] - deletes the excluded data from the original data object
*/
const exclude = (values, deleteFromData = true) => {
for (const [language, langValues] of Object.entries(values)) {
for (const [index, {path}] of Object.entries(langValues)) {
if (path[path.length - 1].includes(_excludeBubble)) {
delete langValues[index];
if (deleteFromData) delete data[language][index];
}
}
}
}
if (_excludeBubble) {
exclude(meta);
exclude(cache, false);
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {Help4.engine.MLTEngine.TranslationData} translationData
* @returns {Promise<?Object>}
*/
async function _getTranslations({data, meta, cache}) {
// ongoing translation. wait for it to finish. domRefresh will call again.
if (this._xhrPromiseContainer) return null;
// exclude bubble if it is turned off from bubble button
_excludeBubbleData.call(this, {data, meta, cache});
const {TRANSLATION_STATES: states} = this.constructor;
const newTexts = {};
const handleCached = () => {
if (!Object.keys(cache).length) return;
for (const [language, langCache] of Object.entries(cache)) {
for (const {path, translation, original} of Object.values(langCache)) {
_addToTexts(path, translation, newTexts);
// save to cache in case it is retrieved by text and not with id e.g. bubbles
_saveToCache.call(this, path, translation, original, language);
}
}
}
let status = null;
const {_targetLanguage, _languageMap} = this;
// skip unsupported source languages
for (const lang of Object.keys(data)) {
if (!_languageMap[lang]?.includes(_targetLanguage)) {
status = states.partial;
delete data[lang];
}
}
if (!Object.keys(data).length) {
handleCached();
_handleTranslationStatus.call(this, states.skipped);
return newTexts;
}
// abort protection
for (const langData of Object.values(data)) {
if (!langData.length || langData.findIndex(val => val == null) > -1) return null;
}
const {controller} = this;
_setPanelAnimation.call(this, true);
const responses = /** @type {Help4.engine.MLTEngine.ServerResponse[]} */ await _callMLTranslation.call(this, data);
if (this._xhrError) {
_handleTranslationStatus.call(this, states.fail);
return null;
}
if (!responses.length || this.isDestroyed()) return null;
const updateStatus = update => {
if (update) {
if (status === null) status = states.success;
if (status !== states.success) status = states.partial;
} else {
if (status === null) status = states.fail;
if (status !== states.fail) status = states.partial;
}
}
const {translation: {retryFailedMLTranslations}} = controller.getConfiguration();
for (const response of responses) {
const {content, sourceLanguage} = response;
if (!content) {
updateStatus(false);
continue;
}
for (const [index, translatedObject] of content.entries()) {
const {data, status} = translatedObject;
const {path, text} = meta[sourceLanguage][index];
if (status !== 200) {
updateStatus(false);
// cache original to skip trying again
retryFailedMLTranslations || _saveToCache.call(this, path, text, text, sourceLanguage);
continue;
}
updateStatus(true);
_saveToCache.call(this, path, data, text, sourceLanguage);
_addToTexts(path, data, newTexts);
}
}
handleCached();
_handleTranslationStatus.call(this, status);
_cleanXhrPromise.call(this);
_setPanelAnimation.call(this, false);
return newTexts;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {boolean} animate
*/
function _setPanelAnimation(animate) {
const {_activeBubble, controller} = this;
const panel = controller.getPanel();
if (!_activeBubble) panel.animateTranslationButton = animate;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {Object} data - {en-US: Help4.engine.MLTEngine.ContentObject[], ...}
* @returns {Promise<Help4.engine.MLTEngine.ServerResponse[]>}
*/
async function _callMLTranslation(data) {
const {SEN, Core} = Help4.widget.companionCore;
const {_targetLanguage: targetLanguage} = this;
const {serverBaseUrl, translation} = _getUrls.call(this);
const url = serverBaseUrl + translation;
const promises = new Help4.XhrPromiseContainer();
for (let [sourceLanguage, content] of Object.entries(data)) {
content = content.map(({contentType, data}) => ({contentType, data})); // remove language to reduce payload
const promise = Help4.ajax.Ajax({
url,
method: 'POST',
data: {
sourceLanguage,
targetLanguage,
content
},
saml: true,
xcsrf: SEN.getXCSRF(serverBaseUrl),
xhrPromise: true
});
const onComplete = () => {
const xhr = promise.getXhr();
Core.broadcastXhrStatus(xhr);
if (xhr.error) this._xhrError = true;
}
promise
.then(onComplete)
.catch(onComplete);
promises.add(promise);
}
this._xhrPromiseContainer = promises.then(data => data?.response?.translation || {}, () => null);
return await this._xhrPromiseContainer.all();
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {Help4.engine.MLTEngine.TRANSLATION_STATES} status
* @return {Promise<void>}
*/
async function _handleTranslationStatus(status) {
const {
control2: {InfoBar: {TYPES: {warning, error}}},
Localization
} = Help4;
const {controller, constructor} = this;
const {TRANSLATION_STATES: states} = constructor;
const infobar = controller.getService('infobar4');
if (status !== states.skipped) {
if (status !== states.fail) {
const disclaimerShown = await _showMLTranslationDisclaimer.call(this);
if (!disclaimerShown && status === states.partial) await _showPartialDisclaimer.call(this);
} else {
infobar.add({type: error, content: Localization.getText('label.translationfail')});
_setPanelAnimation.call(this, false);
this._translationActive = false;
_setButtonStates.call(this, false);
}
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<boolean>} - true if disclaimer was shown
*/
async function _showMLTranslationDisclaimer() {
const {Localization} = Help4;
const {controller} = this;
const storageService = controller.getService('storage');
const infobar = controller.getService('infobar4');
const alreadyShown = await storageService.get(Help4.SERIALIZE_MLTRANSLATION_KEY, {target: 'session'});
if (!alreadyShown) {
infobar.add({content: Localization.getText('label.translationdisclaimer'), timeout: 0});
storageService.set(Help4.SERIALIZE_MLTRANSLATION_KEY, 1, {target: 'session'});
}
return !alreadyShown;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<void>}
*/
async function _showPartialDisclaimer() {
const {
control2: {InfoBar: {TYPES: {warning}}},
Localization
} = Help4;
const {controller} = this;
const storageService = controller.getService('storage');
const infobar = controller.getService('infobar4');
if (!await storageService.get(Help4.SERIALIZE_MLT_MISMATCH_KEY, {target: 'session'})) {
infobar.add({type: warning, content: Localization.getText('label.translationpartialfail')});
storageService.set(Help4.SERIALIZE_MLT_MISMATCH_KEY, 1, {target: 'session'});
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {string[]} path
* @param {string} text
* @param {Object} texts
*/
function _addToTexts(path, text, texts) {
let obj = texts;
for (let i = 0; i < path.length; i++) {
const id = path[i];
obj[id] ||= {};
if (i < path.length - 1) {
obj = obj[id];
} else {
obj[id] = text;
}
}
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {Object} texts
* @returns {Promise<void>}
*/
async function _setTextsToWidgets(texts) {
const {_activeBubble} = this;
if (!Object.keys(texts).length) return;
_activeBubble
? await _activeBubble?.setTexts(texts)
: await Help4.widget.setTexts(texts);
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @property {boolean} [abort = false]
*/
function _cleanXhrPromise(abort = false) {
const {_xhrPromiseContainer} = this;
if (!_xhrPromiseContainer) return;
_setPanelAnimation.call(this, false);
abort && _xhrPromiseContainer.abort();
_xhrPromiseContainer.destroy();
this._xhrPromiseContainer = null;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {string[]} path
* @param {string} translation
* @param {string} text
* @param {string} language
*/
function _saveToCache(path, translation, text, language) {
const {_cache, _targetLanguage} = this;
let id = path[path.length - 1];
// XRAY-6357
if (CACHE_EXEMPTIONS.find(exempId => id.includes(exempId))) {
id = `${id}-${text}`;
}
_cache[language] ||= {};
_cache[language][_targetLanguage] ||= {};
_cache[language][_targetLanguage][id] = {translation, original: text};
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @param {string} language
* @param {string} id
* @param {string} [text = '']
* @returns {{translation: ?string, original: ?string}}
*/
function _getFromCache(language, id = '', text = '') {
const {_cache, _targetLanguage} = this;
const cache = _cache[language]?.[_targetLanguage];
const failObj = {translation: null, original: null};
if (cache) {
// XRAY-6357
if (CACHE_EXEMPTIONS.find(exID => id.includes(exID))) {
return Object.values(cache).find(({translation, original}) => translation === text || original === text) || failObj;
}
// first try to find it from id
if (cache[id]) return cache[id];
if (text) {
// if not found, try to find it from text
// dom elements might be created with new ids, so we need to find it from text e.g. bubbles
const item = Object.values(cache).find(({original}) => original === text);
if (item) return item;
}
}
return failObj;
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Promise<boolean>}
*/
async function _checkPreRequirements() {
const {SERVICE_LAYER: {uacp}, widget: {companionCore: {SEN}}} = Help4;
const {controller} = this;
const config = /** @type {Help4.typedef.SystemConfiguration} */ controller.getConfiguration();
const {
help: {serviceLayer},
translation: {allowMLTranslations},
CMP4
} = config;
if (!allowMLTranslations || serviceLayer === uacp || !CMP4) return false;
const {serverBaseUrl, feature, permission} = _getUrls.call(this);
const request = /** @type {Array<{url: string}>} */ [feature, permission].map(url => ({url}));
const [
/** @type {Help4.engine.MLTEngine.SupportedFeaturesResponse} */ supportedFeatures,
/** @type {Help4.engine.MLTEngine.PermissionsResponse} */ permissions
] = await SEN.doMultifileRequest(config, {serverBaseUrl, request}) || [];
return !!supportedFeatures?.response?.feature.find(feature => feature === 'machine_translation') &&
!!permissions?.response?.permission.find(permission => permission === 'mlt_enabled');
}
/**
* @memberof Help4.engine.MLTEngine#
* @private
* @returns {Help4.engine.MLTEngine.UrlConfig}
*/
function _getUrls() {
const {SERVICE_LAYER: {wpb}} = Help4;
const {controller} = this;
const {help: {serviceUrl, serviceUrl2, serviceLayer}} = controller.getConfiguration();
const {SEN} = Help4.widget.companionCore;
const serverBaseUrl = SEN.getServerBaseUrl(serviceLayer === wpb ? serviceUrl : serviceUrl2);
return {
serverBaseUrl,
translation: '/ml_translation_sync',
feature: '/server/supported_features',
permission: '/self/permission',
supportedLanguages: '/ml_supported_languages'
};
}
})();