Files
2026-09-15 10:35:56 +08:00

587 lines
23 KiB
JavaScript

define([
'jquery',
'Magento_Ui/js/modal/modal',
'Magento_Ui/js/modal/alert',
'mage/translate'
], function ($, modal, alert, $t) {
'use strict';
var modalSelector = '#esun-mlt-modal';
function showError(message) {
alert({content: message});
}
function storeHeading(store) {
var parts = [];
if (store && store.label) {
parts.push(store.label);
}
if (store && store.website && store.website !== store.label) {
parts.push(store.website);
}
return parts.join(' / ') || $t('Default Store View');
}
function ajaxJson(options) {
return new Promise(function (resolve, reject) {
$.ajax(options).done(resolve).fail(reject);
});
}
function createModal(config) {
var $existing = $(modalSelector);
if ($existing.length) {
try {
$existing.modal('closeModal').modal('destroy');
} catch (error) {
// The old element may already have been destroyed by Magento UI.
}
$existing.remove();
}
var stores = [],
providers = [],
loadedTargets = [],
$root = $('<div/>', {id: 'esun-mlt-modal'}),
$intro = $('<div/>', {class: 'esun-mlt-intro'}),
$settings = $('<details/>', {class: 'esun-mlt-settings'}),
$settingsSummary = $('<summary/>', {class: 'esun-mlt-settings__summary'}),
$settingsTitle = $('<strong/>').text($t('Translation Settings')),
$settingsMeta = $('<span/>').text($t('Loading store views...')),
$selectorArea = $('<div/>', {class: 'esun-mlt-selector'}),
$storeGroup = $('<div/>', {class: 'esun-mlt-selector__field'}),
$storeLabel = $('<label/>', {for: 'esun-mlt-target-store'}).text($t('Target Store Views')),
$storeHelp = $('<div/>', {class: 'note'}).text(
$t('All target store views are loaded automatically. You can select fewer views and reload if needed.')
),
$storeSelect = $('<select/>', {
id: 'esun-mlt-target-store',
class: 'admin__control-multiselect',
multiple: 'multiple',
size: 5
}),
$providerGroup = $('<div/>', {class: 'esun-mlt-selector__provider'}),
$providerLabel = $('<label/>', {for: 'esun-mlt-provider'}).text($t('Translation Provider')),
$providerSelect = $('<select/>', {
id: 'esun-mlt-provider',
class: 'admin__control-select'
}),
$load = $('<button/>', {type: 'button', class: 'action-primary'}).text($t('Load Fields')),
$workspace = $('<div/>', {class: 'esun-mlt-workspace'}).html(
'<div class="message message-notice">' + $t('Loading store views...') + '</div>'
),
$saveArea = $('<div/>', {class: 'esun-mlt-actions'}).hide(),
$autoTranslate = $('<button/>', {type: 'button', class: 'action-secondary'})
.text($t('Auto Translate')),
$save = $('<button/>', {type: 'button', class: 'action-primary'}).text($t('Save All Translations')),
$progress = $('<div/>', {
class: 'esun-mlt-progress',
role: 'status',
'aria-live': 'polite'
}).hide(),
$progressStatus = $('<div/>', {class: 'esun-mlt-progress__status'}),
$progressTrack = $('<div/>', {
class: 'esun-mlt-progress__track',
role: 'progressbar',
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': 0
}),
$progressBar = $('<span/>', {class: 'esun-mlt-progress__bar'}),
$progressSummary = $('<div/>', {class: 'esun-mlt-progress__summary'});
$intro.append(
$('<strong/>').text($t('Entity ID: ') + config.entityId),
$('<span/>').text($t('Compare the source content with one or more target store views.'))
);
$settingsSummary.append($settingsTitle, $settingsMeta);
$storeGroup.append($storeLabel, $storeSelect, $storeHelp);
$providerGroup.append($providerLabel, $providerSelect);
$selectorArea.append($storeGroup, $load);
$settings.append($settingsSummary, $providerGroup, $selectorArea);
$progressTrack.append($progressBar);
$progress.append($progressStatus, $progressTrack, $progressSummary);
$saveArea.append($autoTranslate, $save);
$root.append($intro, $settings, $workspace, $progress, $saveArea);
$('body').append($root);
modal({
type: 'popup',
responsive: true,
innerScroll: true,
title: $t('Translate') + ' ' + String(config.entityType || '').toUpperCase(),
modalClass: 'esun-mlt-modal',
buttons: []
}, $root);
function sourceStore() {
var sourceId = parseInt(config.sourceStoreId || 0, 10),
match = null;
$.each(stores, function (index, store) {
if (parseInt(store.store_id, 10) === sourceId) {
match = store;
return false;
}
});
return match || {
store_id: 0,
code: 'default',
label: $t('Default Store View'),
website: ''
};
}
function updateSettingsSummary() {
var selectedCount = ($storeSelect.val() || []).length;
$settingsMeta.text(
selectedCount + ' ' + $t('Store Views Selected')
);
}
function fieldControl(field, value, readonly, targetStoreId) {
var rows = field.is_html ? 12 : 3,
$control = $('<textarea/>', {
class: 'admin__control-textarea esun-mlt-value',
rows: rows,
readonly: readonly
}).val(value || '');
$control.attr('data-code', field.code || '');
$control.attr('data-is-html', field.is_html ? '1' : '0');
if (targetStoreId) {
$control.attr('data-store-id', targetStoreId);
}
return $control;
}
function fieldCell(field, value, readonly, targetStoreId) {
var $cell = $('<div/>', {class: 'esun-mlt-cell'}),
$label = $('<label/>', {class: 'esun-mlt-field-label'}).text(field.label || field.code || ''),
$control = fieldControl(field, value, readonly, targetStoreId),
$inherit;
$label.append($('<small/>').text(field.code || ''));
$cell.append($label, $control);
if (!readonly && targetStoreId) {
$inherit = $('<label/>', {class: 'esun-mlt-use-default'}).append(
$('<input/>', {
type: 'checkbox',
class: 'esun-mlt-use-default-checkbox',
checked: field.use_default === true
}).attr({
'data-code': field.code || '',
'data-store-id': targetStoreId
}),
$('<span/>').text($t('Use Default Value'))
);
$control.prop('disabled', field.use_default === true);
$cell.append($inherit);
}
if (field.is_html) {
$cell.append($('<span/>', {class: 'esun-mlt-html-badge'}).text('HTML'));
}
return $cell;
}
function renderComparison(results) {
var sourceFields = results.length ? (results[0].fields || []) : [],
columnCount = results.length + 1,
$grid = $('<div/>', {class: 'esun-mlt-grid'}).css(
'grid-template-columns',
'repeat(' + columnCount + ', minmax(340px, 1fr))'
);
loadedTargets = results;
$workspace.empty();
$saveArea.hide();
if (!sourceFields.length) {
$workspace.append(
$('<div/>', {class: 'message message-warning'}).text($t('No translatable fields found.'))
);
return;
}
$grid.append(
$('<div/>', {class: 'esun-mlt-column-heading esun-mlt-column-heading--source'})
.append($('<strong/>').text(storeHeading(sourceStore())))
.append($('<span/>').text($t('Source')))
);
$.each(results, function (index, result) {
$grid.append(
$('<div/>', {class: 'esun-mlt-column-heading'})
.append($('<strong/>').text(storeHeading(result.store)))
.append($('<span/>').text(result.store.code))
);
});
$.each(sourceFields, function (fieldIndex, sourceField) {
$grid.append(fieldCell(sourceField, sourceField.source_value, true, null));
$.each(results, function (resultIndex, result) {
var targetField = (result.fields || [])[fieldIndex] || sourceField;
$grid.append(
fieldCell(
targetField,
targetField.target_value,
false,
result.store.store_id
)
);
});
});
$workspace.append($grid);
$saveArea.show();
}
function selectedStores() {
var selectedIds = $.map($storeSelect.val() || [], function (value) {
return parseInt(value, 10);
});
return $.grep(stores, function (store) {
return selectedIds.indexOf(parseInt(store.store_id, 10)) !== -1;
});
}
function loadFields() {
var targets = selectedStores(),
requests;
if (!targets.length) {
showError($t('Please select at least one target store view.'));
return;
}
$load.prop('disabled', true);
$workspace.html('<div class="message message-notice">' + $t('Loading fields...') + '</div>');
$saveArea.hide();
requests = $.map(targets, function (store) {
return ajaxJson({
url: config.fieldsUrl,
method: 'GET',
dataType: 'json',
data: {
form_key: window.FORM_KEY,
entity_type: config.entityType,
entity_id: config.entityId,
source_store_id: config.sourceStoreId || 0,
target_store_id: store.store_id
}
}).then(function (response) {
if (!response || response.success !== true) {
throw new Error(response && response.message ? response.message : $t('Failed to load fields.'));
}
return {store: store, fields: response.fields || []};
});
});
Promise.all(requests).then(renderComparison).catch(function (error) {
$workspace.empty();
showError(error.message || $t('Failed to load fields. Please check permission and try again.'));
}).finally(function () {
$load.prop('disabled', false);
});
}
function valuesForStore(storeId) {
var values = {};
$workspace.find('.esun-mlt-value[data-store-id="' + storeId + '"]').each(function () {
values[$(this).attr('data-code')] = $(this).val();
});
return values;
}
function sourceFieldsForTranslation() {
var values = {};
$workspace.find('.esun-mlt-value[readonly]').each(function () {
var $field = $(this),
code = $field.attr('data-code');
if (!code || $field.val() === '') {
return;
}
values[code] = {
value: $field.val(),
is_html: $field.attr('data-is-html') === '1' ? 1 : 0
};
});
return values;
}
function applyAutomaticTranslations(storeId, translations) {
$.each(translations || {}, function (code, value) {
var $control = $workspace.find(
'.esun-mlt-value[data-store-id="' + storeId + '"][data-code="' + code + '"]'
),
$inherit = $workspace.find(
'.esun-mlt-use-default-checkbox[data-store-id="' + storeId + '"][data-code="' + code + '"]'
);
$inherit.prop('checked', false);
$control.prop('disabled', false).val(value);
});
}
function updateTranslationProgress(completed, total, status, successCount, failureCount) {
var percent = total ? Math.round((completed / total) * 100) : 0;
$progress.show();
$progressStatus.text(status);
$progressBar.css('width', percent + '%');
$progressTrack.attr('aria-valuenow', percent);
$progressSummary.text(
$t('Completed') + ': ' + completed + '/' + total + ' · ' +
$t('Successful') + ': ' + successCount + ' · ' +
$t('Failed') + ': ' + failureCount
);
}
function autoTranslateFields() {
var fields = sourceFieldsForTranslation(),
provider = $providerSelect.val(),
providerLabel = $providerSelect.find(':selected').text(),
total = loadedTargets.length,
successCount = 0,
failures = [];
if (!provider) {
showError($t('Please enable and select a translation provider.'));
return;
}
if (!loadedTargets.length || $.isEmptyObject(fields)) {
showError($t('No source fields are available for automatic translation.'));
return;
}
$autoTranslate.prop('disabled', true);
$save.prop('disabled', true);
$load.prop('disabled', true);
$progress.removeClass('esun-mlt-progress--success esun-mlt-progress--warning');
updateTranslationProgress(0, total, $t('Preparing translation...'), 0, 0);
function translateStore(index) {
var result,
storeName;
if (index >= total) {
return Promise.resolve();
}
result = loadedTargets[index];
storeName = storeHeading(result.store);
$autoTranslate.text($t('Translating') + ' ' + (index + 1) + '/' + total);
updateTranslationProgress(
index,
total,
$t('Translating') + ' ' + (index + 1) + '/' + total + ': ' + storeName,
successCount,
failures.length
);
return ajaxJson({
url: config.autoTranslateUrl,
method: 'POST',
dataType: 'json',
data: {
form_key: window.FORM_KEY,
provider: provider,
entity_type: config.entityType,
entity_id: config.entityId,
source_store_id: config.sourceStoreId || 0,
target_store_id: result.store.store_id,
fields: fields
}
}).then(function (response) {
if (!response || response.success !== true) {
throw new Error(
response && response.message ? response.message : $t('Automatic translation failed.')
);
}
applyAutomaticTranslations(result.store.store_id, response.translations || {});
successCount++;
}).catch(function (error) {
failures.push({
store: storeName,
message: error.message || $t('Automatic translation failed.')
});
}).then(function () {
updateTranslationProgress(
index + 1,
total,
$t('Processed') + ' ' + (index + 1) + '/' + total + ': ' + storeName,
successCount,
failures.length
);
return translateStore(index + 1);
});
}
translateStore(0).then(function () {
var status;
if (failures.length) {
status = providerLabel + ': ' + $t('Translation finished with errors.') + ' ' +
failures.map(function (failure) {
return failure.store + ' — ' + failure.message;
}).join(' | ');
$progress.addClass('esun-mlt-progress--warning');
} else {
status = providerLabel + ': ' +
$t('Translation completed. Review the result, then save translations.');
$progress.addClass('esun-mlt-progress--success');
}
updateTranslationProgress(total, total, status, successCount, failures.length);
}).finally(function () {
$autoTranslate.prop('disabled', false).text($t('Auto Translate'));
$save.prop('disabled', false);
$load.prop('disabled', false);
});
}
function useDefaultForStore(storeId) {
var values = {};
$workspace.find('.esun-mlt-use-default-checkbox[data-store-id="' + storeId + '"]').each(function () {
values[$(this).attr('data-code')] = $(this).is(':checked') ? 1 : 0;
});
return values;
}
function saveFields() {
var requests;
if (!loadedTargets.length) {
return;
}
$save.prop('disabled', true);
requests = $.map(loadedTargets, function (result) {
return ajaxJson({
url: config.applyUrl,
method: 'POST',
dataType: 'json',
data: {
form_key: window.FORM_KEY,
entity_type: config.entityType,
entity_id: config.entityId,
source_store_id: config.sourceStoreId || 0,
target_store_id: result.store.store_id,
fields: valuesForStore(result.store.store_id),
use_default: useDefaultForStore(result.store.store_id)
}
}).then(function (response) {
if (!response || response.success !== true) {
throw new Error(response && response.message ? response.message : $t('Save failed.'));
}
return response;
});
});
Promise.all(requests).then(function () {
alert({content: $t('All translations were saved successfully.')});
loadFields();
}).catch(function (error) {
showError(error.message || $t('Save failed.'));
}).finally(function () {
$save.prop('disabled', false);
});
}
$load.on('click', function () {
updateSettingsSummary();
$settings.prop('open', false);
loadFields();
});
$storeSelect.on('change', updateSettingsSummary);
$providerSelect.on('change', updateSettingsSummary);
$autoTranslate.on('click', autoTranslateFields);
$save.on('click', saveFields);
$workspace.on('change', '.esun-mlt-use-default-checkbox', function () {
var $checkbox = $(this),
storeId = $checkbox.attr('data-store-id'),
code = $checkbox.attr('data-code');
$workspace.find(
'.esun-mlt-value[data-store-id="' + storeId + '"][data-code="' + code + '"]'
).prop('disabled', $checkbox.is(':checked'));
});
ajaxJson({
url: config.storesUrl,
method: 'GET',
dataType: 'json',
data: {form_key: window.FORM_KEY}
}).then(function (response) {
if (!response || response.success !== true) {
throw new Error($t('Failed to load store views.'));
}
stores = response.stores || [];
providers = response.providers || [];
$storeSelect.empty();
$providerSelect.empty();
$.each(providers, function (index, provider) {
$providerSelect.append(
$('<option/>', {value: provider.code})
.text(provider.label)
.prop('selected', provider.code === response.default_provider)
);
});
if (!providers.length) {
$providerSelect.append(
$('<option/>', {value: ''}).text($t('No enabled provider'))
);
$autoTranslate.prop('disabled', true);
}
var $websiteGroups = {};
$.each(stores, function (index, store) {
var isSource = parseInt(store.store_id, 10) === parseInt(config.sourceStoreId || 0, 10),
website = store.website || $t('Other Website'),
$option,
$group;
if (isSource) {
return;
}
if (!$websiteGroups[website]) {
$websiteGroups[website] = $('<optgroup/>', {label: website});
$storeSelect.append($websiteGroups[website]);
}
$group = $websiteGroups[website];
$option = $('<option/>', {value: store.store_id})
.text(store.label + ' (' + store.code + ' · ' + store.locale + ')')
.prop('selected', true);
$group.append($option);
});
updateSettingsSummary();
loadFields();
}).catch(function (error) {
showError(error.message || $t('Failed to load store views.'));
});
$root.modal('openModal');
}
return createModal;
});