// ==UserScript==
// @name Journal3 多语言编辑助手
// @namespace http://www.webidea.top/
// @version 1.0.2
// @description 为 Journal3 和 OpenCart 后台提供多语言内容同步、批量翻译功能,支持分行同步、一键同步和自动翻译
// @author TamsChan
// @license GPLv3
// @match *://*/*
// @match *://*/admin/index.php?route=journal3/journal3&user_token=*
// @match *://*/admin/index.php?route=catalog/information/edit&user_token=*&information_id=*
// @match *://*/admin/index.php?route=catalog/information/add&user_token=*&information_id=*
// @icon https://wiki.greasespot.net/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_log
// @run-at document-idle
// @downloadURL https://update.greasyfork.icu/scripts/575721/Journal3%20%E5%A4%9A%E8%AF%AD%E8%A8%80%E7%BC%96%E8%BE%91%E5%8A%A9%E6%89%8B.user.js
// @updateURL https://update.greasyfork.icu/scripts/575721/Journal3%20%E5%A4%9A%E8%AF%AD%E8%A8%80%E7%BC%96%E8%BE%91%E5%8A%A9%E6%89%8B.meta.js
// ==/UserScript==
(function () {
'use strict';
// 进度监控管理器
const progressManager = {
activeTasks: 0,
// 确保只有一个遮罩层
ensureMask: function () {
if ($('#translate-progress-mask').length === 0) {
const maskHtml = '
';
const containerHtml = '';
$('body').append(maskHtml + containerHtml);
}
},
// 添加一个进度条
addProgress: function (taskId, total, label) {
this.ensureMask();
this.activeTasks++;
const progressHtml = `
`;
$('#translate-progress-bars').append(progressHtml);
},
// 更新进度
updateProgress: function (taskId, current, total, status) {
const percentage = Math.round((current / total) * 100);
$(`#progress-${taskId}-bar`).css('width', `${percentage}%`);
$(`#progress-${taskId}-info`).text(`${current}/${total} (${percentage}%) - ${status}`);
},
// 移除一个进度条
removeProgress: function (taskId) {
$(`#progress-${taskId}`).remove();
this.activeTasks--;
// 如果所有任务都完成了,移除遮罩层
if (this.activeTasks <= 0) {
setTimeout(() => {
$('#translate-progress-mask').remove();
$('#translate-progress-container').remove();
this.activeTasks = 0;
}, 300);
}
}
};
// 并发请求所有语言的翻译(带进度监控)
function translateToMultipleLanguages(text, originalLang, languages, label = '翻译') {
const total = languages.length;
let completed = 0;
const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// 添加进度条
progressManager.addProgress(taskId, total, label);
const promises = languages.map(lang => {
return new Promise((resolve, reject) => {
const maxRetries = 3;
let attempt = 0;
function doRequest() {
attempt++;
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=${originalLang}&tl=${lang}&q=${encodeURIComponent(text)}`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function (res) {
if (res.status === 200) {
const result = JSON.parse(res.responseText);
const translated = result[0][0][0];
completed++;
progressManager.updateProgress(taskId, completed, total, `${lang} 完成`);
resolve({ language: lang, result: translated });
} else {
if (attempt < maxRetries) {
setTimeout(doRequest, 1000 * attempt);
} else {
completed++;
progressManager.updateProgress(taskId, completed, total, `${lang} 失败`);
reject({ language: lang, error: res.status });
}
}
},
onerror: function () {
if (attempt < maxRetries) {
setTimeout(doRequest, 1000 * attempt);
} else {
completed++;
progressManager.updateProgress(taskId, completed, total, `${lang} 网络错误`);
reject({ language: lang, error: 'network error' });
}
}
});
}
doRequest();
});
});
return Promise.allSettled(promises).then(results => {
progressManager.removeProgress(taskId);
return results.map(r => r.status === 'fulfilled' ? r.value : r.reason);
});
}
if (location.href.indexOf('route=journal3/journal3') != -1) {
GM_addStyle('.ui-input-lang-container{position:relative}.ui-input-lang-container textarea{min-height:100px;width:100%;border:1px solid #007fff;outline:none;padding:8px}.ui-input-lang-container .ui-input-lang-button{position:absolute;right:5px;bottom:5px;z-index:9;display:flex;align-items:center;gap:5px}.ui-input-lang-container .ui-input-lang-button input,.ui-input-lang-container .ui-input-lang-button button,.ui-input-lang-container .ui-input-lang-button select{height:25px}')
function journal3Init() {
observer.disconnect();
if ($('.ui-input-lang').length > 0) {
let optionHtml = '';
$('.ui-input-lang').eq(0).find('>div:not(.ui-input-lang-container) img').each(function () {
const src = $(this).attr('src');
const label = $(this).attr('alt');
const lang = src.split('/')[1];
optionHtml += ``;
})
const placeholder = '分行同步:一行对于一个语言,按顺序,如遇空行则跳过空行语言。\n同步:所有语言内容同步当前输入框内容。\n翻译:将当前输入框内容翻译到其他语言。';
$('.ui-input-lang').each(function () {
if ($(this).find('.ui-input-lang-container').length == 0) {
let input = ''
if ($(this).find('>div:not(.ui-input-lang-container) textarea').length > 0) {
input = '';
}
$(this).prepend(``);
}
});
}
startObserve();
}
// 创建观察器实例,防抖执行 init 方法
let debounceTimer = null;
const observer = new MutationObserver(() => {
// 清除之前的定时器
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// 设置新的防抖定时器
debounceTimer = setTimeout(() => {
journal3Init();
}, 1000);
});
function startObserve() {
// 配置观察选项(监听所有可能的变化)
const config = {
childList: true, // 观察子节点的添加/删除
subtree: true, // 观察所有后代节点
attributes: true, // 观察属性变化
characterData: true // 观察文本内容变化
};
// 开始观察
const appContainer = document.querySelector('#content') || document.body;
observer.observe(appContainer, config);
}
startObserve()
var _GM_registerMenuCommand;
if (typeof GM_registerMenuCommand != 'undefined') {
_GM_registerMenuCommand = GM_registerMenuCommand;
} else if (typeof GM != 'undefined' && typeof GM.registerMenuCommand != 'undefined') {
_GM_registerMenuCommand = GM.registerMenuCommand;
} else {
_GM_registerMenuCommand = (s, f) => { };
}
_GM_registerMenuCommand("插入", () => {
journal3Init();
});
// 直接同步按钮点击事件
$(document).on('click', '.ui-input-lang-button-sync', function () {
const textarea = $(this).closest('.ui-input-lang-container').find('textarea');
if (textarea.val().trim() === '') {
return;
}
const value = textarea.val();
const langTag = $(this).parents('.ui-input-lang');
if (langTag.find('.tabs').length > 0) {
if (langTag.find('.ui-editor').length > 0) {
langTag.find('.tab-items li').each(function () {
$(this).click();
langTag.find('.ui-editor .note-editor').prev('div').summernote('code', value);
})
} else {
langTag.find('.tab-items li').each(function () {
$(this).click();
const input = langTag.find('>div:not(.ui-input-lang-container) textarea').eq(0)[0];
const key = Object.keys(input).find(key => key.startsWith('__reactEventHandlers'));
if (key) {
input[key].onChange({ target: { value: value } });
}
})
}
} else {
langTag.find('>div:not(.ui-input-lang-container) input').each(function () {
const input = this;
const key = Object.keys(input).find(key => key.startsWith('__reactEventHandlers'));
console.log(input);
if (key) {
input[key].onChange({ target: { value: value } });
}
});
}
})
// 分行同步按钮点击事件
$(document).on('click', '.ui-input-lang-button-sync-line', function () {
const textarea = $(this).closest('.ui-input-lang-container').find('textarea');
if (textarea.val().trim() === '') {
return;
}
const value = textarea.val().trim().split('\n');
for (let i = 0; i < value.length; i++) {
const line = value[i];
if (line.trim() === '') {
continue;
}
const langTag = $(this).parents('.ui-input-lang');
if (langTag.find('.tabs').length > 0) {
let lineFormat = $(this).siblings('input').val()
if (langTag.find('.ui-editor').length > 0) {
langTag.find('.tab-items li').eq(i).click();
langTag.find('.ui-editor .note-editor').prev('div').summernote('code', line.replaceAll(lineFormat, '\n'));
} else {
langTag.find('.tab-items li').eq(i).click();
const input = langTag.find('>div:not(.ui-input-lang-container) textarea').eq(0)[0];
const key = Object.keys(input).find(key => key.startsWith('__reactEventHandlers'));
if (key) {
input[key].onChange({ target: { value: line.replaceAll(lineFormat, '\n') } });
}
}
} else {
const input = langTag.find('>div:not(.ui-input-lang-container) input').eq(i)[0];
const key = Object.keys(input).find(key => key.startsWith('__reactEventHandlers'));
if (key) {
input[key].onChange({ target: { value: line } });
}
}
}
})
$(document).on('click', '.ui-input-lang-button-translate', function () {
const textarea = $(this).closest('.ui-input-lang-container').find('textarea');
if (textarea.val().trim() === '') {
return;
}
const value = textarea.val();
const languages = [];
$(this).parents('.ui-input-lang').find('>div:not(.ui-input-lang-container) .lang-flag img').each(function () {
const img = this;
const src = $(img).attr('src');
languages.push(src.split('/')[1]);
});
const originalLang = $(this).closest('.ui-input-lang').find('.ui-input-lang-select').val();
translateToMultipleLanguages(value, originalLang, languages).then(results => {
results.forEach(r => {
if (r.result) {
const langTag = $(this).parents('.ui-input-lang');
if (langTag.find('.tabs').length > 0) {
let lineFormat = $(this).siblings('input').val()
if (langTag.find('.ui-editor').length > 0) {
langTag.find('.tab-items li').eq(languages.indexOf(r.language)).click();
langTag.find('.ui-editor .note-editor').prev('div').summernote('code', r.result.replaceAll(lineFormat, '\n'));
} else {
langTag.find('.tab-items li').eq(languages.indexOf(r.language)).click();
const input = langTag.find('>div:not(.ui-input-lang-container) textarea').eq(0)[0];
const key = Object.keys(input).find(key => key.startsWith('__reactEventHandlers'));
if (key) {
input[key].onChange({ target: { value: r.result.replaceAll(lineFormat, '\n') } });
}
}
} else {
const input = langTag.find('>div:not(.ui-input-lang-container) input').eq(languages.indexOf(r.language))[0];
const key = Object.keys(input).find(key => key.startsWith('__reactEventHandlers'));
if (key) {
input[key].onChange({ target: { value: r.result } });
}
}
}
});
});
})
} else if (location.href.indexOf('route=catalog/information/edit') != -1 || location.href.indexOf('route=catalog/information/add') != -1) {
function informationInit() {
let optionHtml = '';
$('#tab-general #language li img').each(function () {
const src = $(this).attr('src');
const label = $(this).attr('title');
const lang = src.split('/')[1];
optionHtml += ``;
})
$('#tab-seo .table-responsive').before('');
$('#tab-general #language').append('一键同步');
$('#tab-general .tab-content').append(``);
}
informationInit();
$(document).on('click', '#sync-seo', function () {
const seo = $("#all-seo").val().trim();
if (seo !== '') {
$('input[name^="information_seo_url["]').each(function () {
const src = $(this).siblings('.input-group-addon').find('img').attr('src');
const lang = src.split('/')[1].toLocaleLowerCase();
const langL = lang.split('-');
let langCode = '-' + langL[0];
switch (lang) {
case 'en-gb':
langCode = '';
break;
case 'pt-br':
langCode = '-pt-br';
break;
case 'ko-kr':
langCode = '-kr';
break;
case 'el-gr':
langCode = '-gr';
break;
case 'pt-pt':
langCode = '-pt-pt';
break;
default:
langCode = '-' + langL[0];
break;
}
$(this).val(seo + langCode);
})
$('input[name^="information_seo_url["]').each(function () {
const src = $(this).siblings('.input-group-addon').find('img').attr('src');
const _lang = src.split('/')[1].toLocaleLowerCase();
const _this = this
$('input[name^="information_seo_url["]').each(function () {
const src = $(this).siblings('.input-group-addon').find('img').attr('src');
const lang = src.split('/')[1].toLocaleLowerCase();
if (this !== _this && $(this).val() === $(_this).val()) {
$(this).val(seo + '-' + lang);
$(_this).val(seo + '-' + _lang);
}
})
})
const seoErr = []
$('input[name^="information_seo_url["]').parents('.input-group').removeClass('has-error');
$('input[name^="information_seo_url["]').each(function () {
const _this = this
$('input[name^="information_seo_url["]').each(function () {
if (this !== _this && $(this).val() === $(_this).val()) {
$(this).parents('.input-group').addClass('has-error');
$(_this).parents('.input-group').addClass('has-error');
seoErr.push(true);
}
})
})
if (seoErr.includes(true)) {
alert('SEO URL 重复,请检查');
}
}
})
$(document).on('click', '#sync-line-btn', function () {
const title = $("#input-sync-line").val().trim();
const description = $("#input-description-sync-line").val().trim();
const metaTitle = $("#input-meta-sync-line").val().trim();
const metaDescription = $("#input-meta-description-sync-line").val().trim();
const metaKeywords = $("#input-meta-keywords-sync-line").val().trim();
if (title.trim() !== '') {
$('input[name^="information_description["][name$="][title]"]').val(title);
}
if (description.trim() !== '') {
$('textarea[name^="information_description["][name$="][description]"]').each(function () {
$(this).summernote('code', description);
})
}
if (metaTitle.trim() !== '') {
$('input[name^="information_description["][name$="][meta_title]"]').val(metaTitle);
}
if (metaDescription.trim() !== '') {
$('textarea[name^="information_description["][name$="][meta_description]"]').val(metaDescription);
}
if (metaKeywords.trim() !== '') {
$('textarea[name^="information_description["][name$="][meta_keyword]"]').val(metaKeywords);
}
})
$(document).on('click', '#translate-btn', function () {
const languages = []
$('#tab-general #language li img').each(function () {
const src = $(this).attr('src');
const lang = src.split('/')[1];
languages.push(lang);
})
const originalLanguage = $("#input-original-language").val();
const title = $("#input-sync-line").val().trim();
const description = $("#input-description-sync-line").val().trim();
const metaTitle = $("#input-meta-sync-line").val().trim();
const metaDescription = $("#input-meta-description-sync-line").val().trim();
const metaKeywords = $("#input-meta-keywords-sync-line").val().trim();
if (title !== '') {
$('input[name^="information_description["][name$="][title]"]').val(title);
translateToMultipleLanguages(title, originalLanguage, languages, '标题').then(function (results) {
results.forEach(r => {
if (r.result) {
const language = r.language;
const result = r.result;
const langIndex = languages.indexOf(language);
$('.tab-content .tab-pane').eq(langIndex + 1).find('input[name^="information_description["][name$="][title]"]').val(result);
}
})
})
}
if (description !== '' && description.length < 5000) {
translateToMultipleLanguages(description, originalLanguage, languages, '描述').then(function (results) {
results.forEach(r => {
if (r.result) {
const language = r.language;
const result = r.result;
const langIndex = languages.indexOf(language);
$('.tab-content .tab-pane').eq(langIndex + 1).find('textarea[name^="information_description["][name$="][description]"]').eq(0).summernote('code', result);
}
})
})
}
if (metaTitle !== '') {
translateToMultipleLanguages(metaTitle, originalLanguage, languages, 'meta标题').then(function (results) {
results.forEach(r => {
if (r.result) {
const language = r.language;
const result = r.result;
const langIndex = languages.indexOf(language);
$('.tab-content .tab-pane').eq(langIndex + 1).find('input[name^="information_description["][name$="][meta_title]"]').val(result);
}
})
})
}
if (metaDescription !== '') {
translateToMultipleLanguages(metaDescription, originalLanguage, languages, 'meta描述').then(function (results) {
results.forEach(r => {
if (r.result) {
const language = r.language;
const result = r.result;
const langIndex = languages.indexOf(language);
$('.tab-content .tab-pane').eq(langIndex + 1).find('textarea[name^="information_description["][name$="][meta_description]"]').val(result);
}
})
})
}
if (metaKeywords !== '') {
translateToMultipleLanguages(metaKeywords, originalLanguage, languages, 'meta关键词').then(function (results) {
results.forEach(r => {
if (r.result) {
const language = r.language;
const result = r.result;
const langIndex = languages.indexOf(language);
$('.tab-content .tab-pane').eq(langIndex + 1).find('textarea[name^="information_description["][name$="][meta_keyword]"]').val(result);
}
})
})
}
})
} else if (location.href.indexOf('route=catalog/new_product/edit') != -1 || location.href.indexOf('route=catalog/new_product/add') != -1) {
function initProductDescription() {
let optionHtml = '';
$('#tab-general img').each(function () {
const src = $(this).attr('src');
const label = $(this).attr('title');
const lang = src.split('/')[1];
optionHtml += ``;
})
$('#tab-general').prepend(``)
}
initProductDescription();
$(document).on('click', '#sync-title', function () {
// 同步产品名称
const title = $("#input-sync-line").val().trim();
if (title !== '') {
$('input[name^="product_description["][name$="][name]"]').val(title);
}
})
$(document).on('click', '#translate-btn', function () {
// 翻译产品名称
const title = $("#input-sync-line").val().trim();
const originalLanguage = $("#input-original-language").val();
const languages = [];
$('#tab-general img').each(function () {
const src = $(this).attr('src');
const lang = src.split('/')[1];
languages.push(lang);
})
if (title !== '') {
translateToMultipleLanguages(title, originalLanguage, languages, '产品名称').then(function (results) {
results.forEach(r => {
if (r.result) {
const language = r.language;
const result = r.result;
const langIndex = languages.indexOf(language);
$('input[name^="product_description["][name$="][name]"]').eq(langIndex).val(result);
}
})
})
}
})
}
})();