v1.0.0
This commit is contained in:
commit
a157afe625
21 changed files with 4623 additions and 0 deletions
389
assets/js/editor.js
Normal file
389
assets/js/editor.js
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
/**
|
||||
* 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<string>} fields Field names to translate ('title', 'content').
|
||||
* @param {string} targetLocale WordPress locale code.
|
||||
* @returns {Promise<Object>} 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();
|
||||
}
|
||||
} )();
|
||||
Loading…
Reference in a new issue