/** * AI Translator (by ROBOTSTXT) — editor UI. * * Adds a translation panel to the Block editor sidebar and a handler for the * Classic editor metabox. Both implementations share the same REST endpoint * and language list. * * @package ROBOTSTXT\AI_Translator * @since 1.0.0 */ ( function () { 'use strict'; function warn( message ) { if ( window.console && typeof window.console.warn === 'function' ) { window.console.warn( '[AI Translator] ' + message ); } } var data = window.aiTranslatorData; if ( ! data ) { warn( 'window.aiTranslatorData is missing; aborting.' ); return; } var apiFetch = window.wp && window.wp.apiFetch ? window.wp.apiFetch : null; var labels = data.i18n || {}; /** * Calls the REST endpoint to translate the selected fields of the saved post. * * @param {Array} fields Field names to translate ('title', 'content'). * @param {string} targetLocale WordPress locale code. * @returns {Promise} Promise resolving to the translated values. */ function requestTranslation( fields, targetLocale ) { if ( ! apiFetch ) { return Promise.reject( new Error( labels.genericError || 'Translation failed.' ) ); } return apiFetch( { path: '/' + data.restNamespace + data.restRoute, method: 'POST', data: { post_id: data.postId, fields: fields, target_locale: targetLocale, }, } ); } function resolveFields( wantTitle, wantContent ) { var fields = []; if ( wantTitle && data.settings.translateTitle ) { fields.push( 'title' ); } if ( wantContent && data.settings.translateContent ) { fields.push( 'content' ); } return fields; } function describeError( error ) { if ( error && typeof error.message === 'string' && error.message ) { return error.message; } return labels.genericError || 'Translation failed.'; } /** * Applies a translation response using the right method for the current editor. * * In the block editor, uses wp.data dispatch (and wp.blocks.parse for content). * In the classic editor, updates the DOM directly (title input + TinyMCE). * * @param {Object} response Translation response. * @param {Document} doc Document where the metabox lives (for classic fallback). */ function applyTranslationResult( response, doc ) { var wp = window.wp || {}; var isBlockCtx = data.context === 'block' && wp.data && wp.data.dispatch; if ( isBlockCtx ) { if ( response.title !== undefined ) { wp.data.dispatch( 'core/editor' ).editPost( { title: response.title } ); } if ( response.content !== undefined ) { if ( wp.blocks && typeof wp.blocks.parse === 'function' ) { var blocks = wp.blocks.parse( response.content ); var blockEd = wp.data.dispatch( 'core/block-editor' ); if ( blockEd && typeof blockEd.resetBlocks === 'function' ) { blockEd.resetBlocks( blocks ); } else { wp.data.dispatch( 'core/editor' ).editPost( { content: response.content } ); } } else { wp.data.dispatch( 'core/editor' ).editPost( { content: response.content } ); } } return; } doc = doc || document; if ( response.title !== undefined ) { var titleInput = doc.getElementById( 'title' ); if ( titleInput ) { titleInput.value = response.title; } } if ( response.content !== undefined ) { applyContentToClassic( response.content, doc ); } } function applyContentToClassic( content, doc ) { doc = doc || document; var textarea = doc.getElementById( 'content' ); if ( textarea ) { textarea.value = content; } if ( window.tinymce && typeof window.tinymce.get === 'function' ) { var editor = window.tinymce.get( 'content' ); if ( editor && typeof editor.setContent === 'function' ) { editor.setContent( content ); } } } // ------------------------------------------------------------------------- // Classic editor integration (metabox). // ------------------------------------------------------------------------- function initClassicEditor() { var container = document.getElementById( 'ai-translator-classic' ); if ( ! container ) { return; } var button = document.getElementById( 'ai-translator-classic-button' ); var select = document.getElementById( 'ai-translator-locale' ); var status = document.getElementById( 'ai-translator-classic-status' ); if ( ! button || ! select ) { return; } function setStatus( text, kind ) { if ( ! status ) { return; } status.textContent = text; status.className = 'ai-translator-status' + ( kind ? ' ai-translator-status--' + kind : '' ); } button.addEventListener( 'click', function () { var locale = select.value; if ( ! locale ) { return; } var fields = resolveFields( data.settings.translateTitle, data.settings.translateContent ); if ( fields.length === 0 ) { setStatus( labels.noFields || '', 'warning' ); return; } button.disabled = true; button.dataset.busy = '1'; setStatus( labels.translating || 'Translating…', 'busy' ); requestTranslation( fields, locale ) .then( function ( response ) { applyTranslationResult( response, document ); setStatus( labels.success || 'Translation applied.', 'success' ); } ) .catch( function ( error ) { setStatus( describeError( error ), 'error' ); } ) .then( function () { button.disabled = false; button.dataset.busy = '0'; } ); } ); } // ------------------------------------------------------------------------- // Block editor integration (PluginSidebar). // ------------------------------------------------------------------------- function initBlockEditor() { var wp = window.wp || {}; if ( ! wp.plugins || ! wp.element || ! wp.components || ! wp.data || ! wp.editor || ! wp.editor.PluginSidebar ) { warn( 'Block editor APIs not available; skipping sidebar registration.' ); return; } var el = wp.element.createElement; var Fragment = wp.element.Fragment; var useState = wp.element.useState; var registerP = wp.plugins.registerPlugin; var sidebar = wp.editor.PluginSidebar; var menuItem = wp.editor.PluginSidebarMoreMenuItem; var components = wp.components; var dataStore = wp.data; function applyNoticeSnackbar( status, message ) { if ( ! dataStore || ! dataStore.dispatch || ! message ) { return; } var noticesStore = dataStore.dispatch( 'core/notices' ); if ( ! noticesStore || typeof noticesStore.createNotice !== 'function' ) { return; } noticesStore.createNotice( status, message, { type: 'snackbar', isDismissible: true, } ); } function Panel() { var localeState = useState( ( data.languages && data.languages[0] ) ? data.languages[0].locale : '' ); var locale = localeState[0]; var setLocale = localeState[1]; var busyState = useState( false ); var busy = busyState[0]; var setBusy = busyState[1]; var noticeState = useState( null ); var notice = noticeState[0]; var setNotice = noticeState[1]; var titleState = useState( !! data.settings.translateTitle ); var translateTitle = titleState[0]; var setTranslateTitle = titleState[1]; var contentState = useState( !! data.settings.translateContent ); var translateContent = contentState[0]; var setTranslateContent = contentState[1]; var coreEditor = dataStore.select( 'core/editor' ); var isDirty = coreEditor && typeof coreEditor.isEditedPostDirty === 'function' ? coreEditor.isEditedPostDirty() : false; if ( ! data.available ) { return el( 'div', { className: 'ai-translator-sidebar' }, el( components.Notice, { status: 'warning', isDismissible: false }, labels.aiUnavailable || '' ) ); } if ( ! data.settings.translateTitle && ! data.settings.translateContent ) { return el( 'div', { className: 'ai-translator-sidebar' }, el( components.Notice, { status: 'info', isDismissible: false }, labels.noFields || '' ) ); } if ( ! data.languages || data.languages.length === 0 ) { return el( 'div', { className: 'ai-translator-sidebar' }, el( components.Notice, { status: 'info', isDismissible: false }, labels.noLanguages || '' ) ); } var options = data.languages.map( function ( language ) { return { value: language.locale, label: language.name }; } ); function runTranslate() { var fields = resolveFields( translateTitle, translateContent ); if ( fields.length === 0 ) { setNotice( { status: 'warning', message: labels.noFields || '' } ); return; } setBusy( true ); setNotice( null ); applyNoticeSnackbar( 'info', labels.translating || 'Translating…' ); requestTranslation( fields, locale ) .then( function ( response ) { applyTranslationResult( response ); setNotice( { status: 'success', message: labels.success || '' } ); applyNoticeSnackbar( 'success', labels.success || 'Translation applied.' ); } ) .catch( function ( error ) { var errorMessage = describeError( error ); setNotice( { status: 'error', message: errorMessage } ); applyNoticeSnackbar( 'error', errorMessage ); } ) .then( function () { setBusy( false ); } ); } var children = []; if ( isDirty ) { children.push( el( components.Notice, { status: 'warning', isDismissible: false, key: 'dirty', }, labels.saveBeforeWarn || '' ) ); } if ( notice ) { children.push( el( components.Notice, { status: notice.status, isDismissible: true, onRemove: function () { setNotice( null ); }, key: 'notice', }, notice.message ) ); } children.push( el( components.SelectControl, { key: 'locale', label: labels.targetLanguage || '', value: locale, options: options, onChange: function ( value ) { setLocale( value ); }, } ) ); if ( data.settings.translateTitle ) { children.push( el( components.CheckboxControl, { key: 'title', label: labels.translateTitle || '', checked: translateTitle, onChange: function ( value ) { setTranslateTitle( !! value ); }, } ) ); } if ( data.settings.translateContent ) { children.push( el( components.CheckboxControl, { key: 'content', label: labels.translateContent || '', checked: translateContent, onChange: function ( value ) { setTranslateContent( !! value ); }, } ) ); } children.push( el( components.Button, { key: 'submit', variant: 'primary', disabled: busy || ! locale, isBusy: busy, onClick: runTranslate, }, busy ? ( labels.translating || '' ) : ( labels.translate || '' ) ) ); return el( 'div', { className: 'ai-translator-sidebar' }, children ); } registerP( 'ai-translator', { render: function () { return el( Fragment, null, menuItem ? el( menuItem, { target: 'ai-translator', icon: 'translation' }, labels.panelTitle || '' ) : null, el( sidebar, { name: 'ai-translator', title: labels.panelTitle || '', icon: 'translation' }, el( components.PanelBody, {}, el( Panel ) ) ) ); }, } ); } function start() { if ( data.context === 'block' ) { initBlockEditor(); } else { initClassicEditor(); } } if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', start ); } else { start(); } } )();