// ==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 = `
${label}
0/${total} (0%)
`; $('#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(`
${input}
`); } }); } 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); } }) }) } }) } })();