v1.2.0
This commit is contained in:
parent
02cd01e862
commit
1ccc13da01
14 changed files with 845 additions and 197 deletions
|
|
@ -5,6 +5,10 @@
|
|||
* Classic editor metabox. Both implementations share the same REST endpoint
|
||||
* and language list.
|
||||
*
|
||||
* Content translation is performed in parallel chunks so that long posts do not
|
||||
* hit server-side timeout limits. Title and excerpt are short fields and are
|
||||
* translated in a single request using the standard /translate endpoint.
|
||||
*
|
||||
* @package ROBOTSTXT\AI_Translator
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
|
@ -27,12 +31,201 @@
|
|||
var apiFetch = window.wp && window.wp.apiFetch ? window.wp.apiFetch : null;
|
||||
var labels = data.i18n || {};
|
||||
|
||||
// ── Chunking helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Calls the REST endpoint to translate the selected fields of the saved post.
|
||||
* Splits Gutenberg block HTML into chunks at block boundaries.
|
||||
*
|
||||
* @param {Array<string>} fields Field names to translate ('title', 'content', 'excerpt').
|
||||
* Each chunk ends on a complete Gutenberg block so the AI never receives
|
||||
* partial markup. Chunks are kept below maxChars where possible; a single
|
||||
* block larger than maxChars is sent as its own chunk.
|
||||
*
|
||||
* @param {string} content Serialised Gutenberg block HTML.
|
||||
* @param {number} maxChars Maximum characters per chunk.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
function chunkByBlocks( content, maxChars ) {
|
||||
if ( ! content || content.length <= maxChars ) {
|
||||
return [ content ];
|
||||
}
|
||||
|
||||
// Split just before each block comment opener so every part starts at a
|
||||
// block boundary. The split delimiter is a zero-width lookahead so each
|
||||
// part keeps the opening comment intact.
|
||||
var parts = content.split( /(?=<!-- wp:)/ );
|
||||
|
||||
var chunks = [];
|
||||
var current = '';
|
||||
|
||||
for ( var i = 0; i < parts.length; i++ ) {
|
||||
var part = parts[ i ];
|
||||
if ( '' === part ) {
|
||||
continue;
|
||||
}
|
||||
if ( current.length > 0 && current.length + part.length > maxChars ) {
|
||||
chunks.push( current );
|
||||
current = part;
|
||||
} else {
|
||||
current += part;
|
||||
}
|
||||
}
|
||||
|
||||
if ( current ) {
|
||||
chunks.push( current );
|
||||
}
|
||||
|
||||
return chunks.length ? chunks : [ content ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits HTML content (Classic editor) into chunks at paragraph / heading /
|
||||
* list-item boundaries so the AI never receives partial sentences.
|
||||
*
|
||||
* @param {string} html HTML content.
|
||||
* @param {number} maxChars Maximum characters per chunk.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
function chunkByParagraphs( html, maxChars ) {
|
||||
if ( ! html || html.length <= maxChars ) {
|
||||
return [ html ];
|
||||
}
|
||||
|
||||
// Match block-level closing tags so we can split after them.
|
||||
var regex = /(<\/(?:p|h[1-6]|li|blockquote|pre|div|figure)>)(\s*)/gi;
|
||||
|
||||
var chunks = [];
|
||||
var current = '';
|
||||
var lastIndex = 0;
|
||||
var match;
|
||||
|
||||
while ( ( match = regex.exec( html ) ) !== null ) {
|
||||
var segEnd = match.index + match[0].length;
|
||||
var segment = html.slice( lastIndex, segEnd );
|
||||
lastIndex = segEnd;
|
||||
|
||||
if ( current.length > 0 && current.length + segment.length > maxChars ) {
|
||||
chunks.push( current );
|
||||
current = segment;
|
||||
} else {
|
||||
current += segment;
|
||||
}
|
||||
}
|
||||
|
||||
// Append any trailing content after the last closing tag.
|
||||
if ( lastIndex < html.length ) {
|
||||
current += html.slice( lastIndex );
|
||||
}
|
||||
if ( current ) {
|
||||
chunks.push( current );
|
||||
}
|
||||
|
||||
return chunks.length ? chunks : [ html ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current content from the Classic editor.
|
||||
*
|
||||
* Prefers the TinyMCE visual editor when it is active; falls back to the
|
||||
* raw textarea value when TinyMCE is in source (text) mode or absent.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCurrentClassicContent() {
|
||||
var tinymce = window.tinymce;
|
||||
if ( tinymce && typeof tinymce.get === 'function' ) {
|
||||
var ed = tinymce.get( 'content' );
|
||||
if ( ed && typeof ed.getContent === 'function' ) {
|
||||
// When TinyMCE is hidden the user is in "Text" (source) mode;
|
||||
// in that case the textarea holds the latest value.
|
||||
if ( typeof ed.isHidden === 'function' && ed.isHidden() ) {
|
||||
var textareaSrc = document.getElementById( 'content' );
|
||||
return textareaSrc ? textareaSrc.value : '';
|
||||
}
|
||||
return ed.getContent();
|
||||
}
|
||||
}
|
||||
var textarea = document.getElementById( 'content' );
|
||||
return textarea ? textarea.value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a single text chunk to the /translate-text REST endpoint.
|
||||
*
|
||||
* @param {string} text Text (may include HTML) to translate.
|
||||
* @param {string} targetLocale WordPress locale code.
|
||||
* @returns {Promise<Object>} Promise resolving to the translated values.
|
||||
* @returns {Promise<string>} The translated text.
|
||||
*/
|
||||
function translateChunk( text, targetLocale ) {
|
||||
if ( ! apiFetch ) {
|
||||
return Promise.reject( new Error( labels.genericError || 'Translation failed.' ) );
|
||||
}
|
||||
|
||||
return apiFetch( {
|
||||
path: '/' + data.restNamespace + data.chunkTextRoute,
|
||||
method: 'POST',
|
||||
data: {
|
||||
post_id: data.postId,
|
||||
text: text,
|
||||
target_locale: targetLocale,
|
||||
},
|
||||
} ).then( function ( response ) {
|
||||
return ( response && typeof response.text === 'string' ) ? response.text : '';
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates content by splitting it into chunks and translating them
|
||||
* concurrently via Promise.all().
|
||||
*
|
||||
* Only `content` uses this path. Title and excerpt are short and go through
|
||||
* the standard /translate endpoint which reads from the database.
|
||||
*
|
||||
* @param {string} content Serialised block HTML or Classic editor HTML.
|
||||
* @param {string} targetLocale WordPress locale code.
|
||||
* @param {boolean} isBlock True when the content is Gutenberg block HTML.
|
||||
* @param {Function} onProgress Called with (done, total) after each chunk resolves.
|
||||
* @returns {Promise<string>} Reassembled translated content.
|
||||
*/
|
||||
function translateContentParallel( content, targetLocale, isBlock, onProgress ) {
|
||||
if ( ! content || '' === content.trim() ) {
|
||||
return Promise.resolve( '' );
|
||||
}
|
||||
|
||||
var chunkSize = ( data.chunkSize && data.chunkSize > 0 ) ? data.chunkSize : 5000;
|
||||
var chunks = isBlock
|
||||
? chunkByBlocks( content, chunkSize )
|
||||
: chunkByParagraphs( content, chunkSize );
|
||||
var total = chunks.length;
|
||||
var done = 0;
|
||||
|
||||
if ( typeof onProgress === 'function' ) {
|
||||
onProgress( 0, total );
|
||||
}
|
||||
|
||||
var promises = chunks.map( function ( chunk ) {
|
||||
return translateChunk( chunk, targetLocale ).then( function ( translated ) {
|
||||
done++;
|
||||
if ( typeof onProgress === 'function' ) {
|
||||
onProgress( done, total );
|
||||
}
|
||||
return translated;
|
||||
} );
|
||||
} );
|
||||
|
||||
return Promise.all( promises ).then( function ( parts ) {
|
||||
return parts.join( '' );
|
||||
} );
|
||||
}
|
||||
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Calls the /translate endpoint to translate title and/or excerpt fields of
|
||||
* the saved post.
|
||||
*
|
||||
* @param {Array<string>} fields Field names ('title', 'excerpt').
|
||||
* @param {string} targetLocale WordPress locale code.
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
function requestTranslation( fields, targetLocale ) {
|
||||
if ( ! apiFetch ) {
|
||||
|
|
@ -50,14 +243,11 @@
|
|||
} );
|
||||
}
|
||||
|
||||
function resolveFields( wantTitle, wantContent, wantExcerpt ) {
|
||||
function resolveMetaFields( wantTitle, wantExcerpt ) {
|
||||
var fields = [];
|
||||
if ( wantTitle && data.settings.translateTitle ) {
|
||||
fields.push( 'title' );
|
||||
}
|
||||
if ( wantContent && data.settings.translateContent ) {
|
||||
fields.push( 'content' );
|
||||
}
|
||||
if ( wantExcerpt && data.settings.translateExcerpt ) {
|
||||
fields.push( 'excerpt' );
|
||||
}
|
||||
|
|
@ -72,59 +262,32 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* Applies a translation response using the right method for the current editor.
|
||||
* Applies translated content to the Block 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 + excerpt textarea).
|
||||
*
|
||||
* @param {Object} response Translation response.
|
||||
* @param {Document} doc Document where the metabox lives (for classic fallback).
|
||||
* @param {string} content Serialised block HTML.
|
||||
*/
|
||||
function applyTranslationResult( response, doc ) {
|
||||
function applyContentToBlockEditor( content ) {
|
||||
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 ( ! wp.data || ! wp.data.dispatch ) {
|
||||
return;
|
||||
}
|
||||
if ( response.content !== undefined ) {
|
||||
if ( wp.blocks && typeof wp.blocks.parse === 'function' ) {
|
||||
var blocks = wp.blocks.parse( response.content );
|
||||
var blocks = wp.blocks.parse( 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 } );
|
||||
}
|
||||
}
|
||||
if ( response.excerpt !== undefined ) {
|
||||
wp.data.dispatch( 'core/editor' ).editPost( { excerpt: response.excerpt } );
|
||||
}
|
||||
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 );
|
||||
}
|
||||
if ( response.excerpt !== undefined ) {
|
||||
var excerptTextarea = doc.getElementById( 'excerpt' );
|
||||
if ( excerptTextarea ) {
|
||||
excerptTextarea.value = response.excerpt;
|
||||
}
|
||||
}
|
||||
wp.data.dispatch( 'core/editor' ).editPost( { content: content } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies translated content to the Classic editor (textarea + TinyMCE).
|
||||
*
|
||||
* @param {string} content HTML content.
|
||||
* @param {Document} doc Document where the editor lives.
|
||||
*/
|
||||
function applyContentToClassic( content, doc ) {
|
||||
doc = doc || document;
|
||||
var textarea = doc.getElementById( 'content' );
|
||||
|
|
@ -139,9 +302,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Classic editor integration (metabox).
|
||||
// -------------------------------------------------------------------------
|
||||
/**
|
||||
* Applies a translation response (title / excerpt) using the right method
|
||||
* for the current editor. Content is handled separately via chunked translation.
|
||||
*
|
||||
* @param {Object} response Translation response (may contain title, excerpt).
|
||||
* @param {Document} doc Document where the Classic editor lives.
|
||||
*/
|
||||
function applyMetaTranslation( 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.excerpt !== undefined ) {
|
||||
wp.data.dispatch( 'core/editor' ).editPost( { excerpt: response.excerpt } );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
doc = doc || document;
|
||||
if ( response.title !== undefined ) {
|
||||
var titleInput = doc.getElementById( 'title' );
|
||||
if ( titleInput ) {
|
||||
titleInput.value = response.title;
|
||||
}
|
||||
}
|
||||
if ( response.excerpt !== undefined ) {
|
||||
var excerptTextarea = doc.getElementById( 'excerpt' );
|
||||
if ( excerptTextarea ) {
|
||||
excerptTextarea.value = response.excerpt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Classic editor integration (metabox) ──────────────────────────────────
|
||||
|
||||
function initClassicEditor() {
|
||||
var container = document.getElementById( 'ai-translator-classic' );
|
||||
|
|
@ -171,12 +368,13 @@
|
|||
return;
|
||||
}
|
||||
|
||||
var fields = resolveFields(
|
||||
var wantContent = data.settings.translateContent;
|
||||
var metaFields = resolveMetaFields(
|
||||
data.settings.translateTitle,
|
||||
data.settings.translateContent,
|
||||
data.settings.translateExcerpt
|
||||
);
|
||||
if ( fields.length === 0 ) {
|
||||
|
||||
if ( metaFields.length === 0 && ! wantContent ) {
|
||||
setStatus( labels.noFields || '', 'warning' );
|
||||
return;
|
||||
}
|
||||
|
|
@ -185,9 +383,40 @@
|
|||
button.dataset.busy = '1';
|
||||
setStatus( labels.translating || 'Translating…', 'busy' );
|
||||
|
||||
requestTranslation( fields, locale )
|
||||
.then( function ( response ) {
|
||||
applyTranslationResult( response, document );
|
||||
var metaPromise = metaFields.length > 0
|
||||
? requestTranslation( metaFields, locale )
|
||||
: Promise.resolve( null );
|
||||
|
||||
var contentPromise = wantContent
|
||||
? translateContentParallel(
|
||||
getCurrentClassicContent(),
|
||||
locale,
|
||||
false,
|
||||
function ( done, total ) {
|
||||
if ( total > 1 ) {
|
||||
setStatus(
|
||||
( labels.translatingProgress || 'Translating… ({done}/{total})' )
|
||||
.replace( '{done}', String( done ) )
|
||||
.replace( '{total}', String( total ) ),
|
||||
'busy'
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
: Promise.resolve( null );
|
||||
|
||||
Promise.all( [ metaPromise, contentPromise ] )
|
||||
.then( function ( results ) {
|
||||
var metaResult = results[0];
|
||||
var contentResult = results[1];
|
||||
|
||||
if ( metaResult ) {
|
||||
applyMetaTranslation( metaResult, document );
|
||||
}
|
||||
if ( null !== contentResult ) {
|
||||
applyContentToClassic( contentResult, document );
|
||||
}
|
||||
|
||||
setStatus( labels.success || 'Translation applied.', 'success' );
|
||||
} )
|
||||
.catch( function ( error ) {
|
||||
|
|
@ -200,9 +429,7 @@
|
|||
} );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Block editor integration (PluginSidebar).
|
||||
// -------------------------------------------------------------------------
|
||||
// ── Block editor integration (PluginSidebar) ──────────────────────────────
|
||||
|
||||
function initBlockEditor() {
|
||||
var wp = window.wp || {};
|
||||
|
|
@ -248,6 +475,11 @@
|
|||
var busy = busyState[0];
|
||||
var setBusy = busyState[1];
|
||||
|
||||
// progress: null = idle, { done: number, total: number }
|
||||
var progressState = useState( null );
|
||||
var progress = progressState[0];
|
||||
var setProgress = progressState[1];
|
||||
|
||||
var noticeState = useState( null );
|
||||
var notice = noticeState[0];
|
||||
var setNotice = noticeState[1];
|
||||
|
|
@ -290,26 +522,63 @@
|
|||
} );
|
||||
|
||||
function runTranslate() {
|
||||
var fields = resolveFields( translateTitle, translateContent, translateExcerpt );
|
||||
var wantTitle = translateTitle && data.settings.translateTitle;
|
||||
var wantContent = translateContent && data.settings.translateContent;
|
||||
var wantExcerpt = translateExcerpt && data.settings.translateExcerpt;
|
||||
|
||||
if ( fields.length === 0 ) {
|
||||
if ( ! wantTitle && ! wantContent && ! wantExcerpt ) {
|
||||
setNotice( { status: 'warning', message: labels.noFields || '' } );
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy( true );
|
||||
setProgress( null );
|
||||
setNotice( null );
|
||||
applyNoticeSnackbar( 'info', labels.translating || 'Translating…' );
|
||||
|
||||
requestTranslation( fields, locale )
|
||||
.then( function ( response ) {
|
||||
applyTranslationResult( response );
|
||||
// Title + excerpt: single request against the saved post in the DB.
|
||||
var metaFields = resolveMetaFields( wantTitle, wantExcerpt );
|
||||
var metaPromise = metaFields.length > 0
|
||||
? requestTranslation( metaFields, locale )
|
||||
: Promise.resolve( null );
|
||||
|
||||
// Content: chunked parallel translation of the current editor state.
|
||||
var contentSource = wantContent
|
||||
? ( coreEditor && typeof coreEditor.getEditedPostAttribute === 'function'
|
||||
? ( coreEditor.getEditedPostAttribute( 'content' ) || '' )
|
||||
: '' )
|
||||
: null;
|
||||
var contentPromise = ( null !== contentSource && '' !== contentSource )
|
||||
? translateContentParallel(
|
||||
contentSource,
|
||||
locale,
|
||||
true,
|
||||
function ( done, total ) {
|
||||
setProgress( { done: done, total: total } );
|
||||
}
|
||||
)
|
||||
: Promise.resolve( null );
|
||||
|
||||
Promise.all( [ metaPromise, contentPromise ] )
|
||||
.then( function ( results ) {
|
||||
var metaResult = results[0];
|
||||
var contentResult = results[1];
|
||||
|
||||
if ( metaResult ) {
|
||||
applyMetaTranslation( metaResult );
|
||||
}
|
||||
if ( null !== contentResult ) {
|
||||
applyContentToBlockEditor( contentResult );
|
||||
}
|
||||
|
||||
setNotice( { status: 'success', message: labels.success || '' } );
|
||||
setProgress( null );
|
||||
applyNoticeSnackbar( 'success', labels.success || 'Translation applied.' );
|
||||
} )
|
||||
.catch( function ( error ) {
|
||||
var errorMessage = describeError( error );
|
||||
setNotice( { status: 'error', message: errorMessage } );
|
||||
setProgress( null );
|
||||
applyNoticeSnackbar( 'error', errorMessage );
|
||||
} )
|
||||
.then( function () {
|
||||
|
|
@ -317,6 +586,20 @@
|
|||
} );
|
||||
}
|
||||
|
||||
// Compute the button label based on busy/progress state.
|
||||
var buttonLabel;
|
||||
if ( busy ) {
|
||||
if ( progress && progress.total > 1 ) {
|
||||
buttonLabel = ( labels.translatingProgress || 'Translating… ({done}/{total})' )
|
||||
.replace( '{done}', String( progress.done ) )
|
||||
.replace( '{total}', String( progress.total ) );
|
||||
} else {
|
||||
buttonLabel = labels.translating || 'Translating…';
|
||||
}
|
||||
} else {
|
||||
buttonLabel = labels.translate || 'Translate';
|
||||
}
|
||||
|
||||
var children = [];
|
||||
|
||||
if ( isDirty ) {
|
||||
|
|
@ -387,7 +670,7 @@
|
|||
disabled: busy || ! locale,
|
||||
isBusy: busy,
|
||||
onClick: runTranslate,
|
||||
}, busy ? ( labels.translating || '' ) : ( labels.translate || '' ) ) );
|
||||
}, buttonLabel ) );
|
||||
|
||||
return el( 'div', { className: 'ai-translator-sidebar' }, children );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,38 @@
|
|||
== Changelog ==
|
||||
|
||||
= 1.2.0 =
|
||||
|
||||
_Release date: 2026-05-28_
|
||||
|
||||
**Highlights**
|
||||
|
||||
* Parallel chunked content translation: long posts are split at block or paragraph boundaries and all chunks are translated concurrently, eliminating timeout errors.
|
||||
* Configurable request timeout: control how long the server waits for an AI response (30–300 s).
|
||||
|
||||
**Added**
|
||||
|
||||
* Configurable "Request timeout" setting (default 60 s, minimum 30 s, maximum 300 s) in both site and network admin. Controls how long the server waits for a single AI response before timing out. The PHP `max_execution_time` value is shown as a hint.
|
||||
* Parallel chunked content translation — long post content is automatically split at Gutenberg block boundaries (Block editor) or paragraph/heading boundaries (Classic editor) and all chunks are sent to the AI concurrently via `Promise.all()`.
|
||||
* Progress indicator — the Translate button label updates in real time: "Translating… (2/5)" after each chunk resolves.
|
||||
* New REST endpoint `POST /wp-json/ai-translator/v1/translate-text` — translates a raw text string (HTML or plain text) supplied directly in the request body. Used internally by the chunked content path. Requires the same `edit_post` capability as the existing `/translate` endpoint.
|
||||
|
||||
**Changed**
|
||||
|
||||
* Content translation now reads directly from the current editor state (unsaved changes are included) instead of from the last saved post revision. Title and excerpt continue to be read from the database.
|
||||
|
||||
**Compatibility**
|
||||
|
||||
* WordPress: 7.0 - 7.1
|
||||
* PHP: 7.4 - 8.5
|
||||
* WP-CLI: 2.12 or newer
|
||||
|
||||
**Tests**
|
||||
|
||||
* PHP Coding Standards: WordPress-Core, WordPress-Docs, WordPress-Extra
|
||||
* PHPCompatibility: 8.2 - 8.5
|
||||
* PHPStan: level 9
|
||||
* PHPUnit: 51/51 tests passing (single-site)
|
||||
|
||||
= 1.1.0 =
|
||||
|
||||
_Release date: 2026-05-25_
|
||||
|
|
|
|||
|
|
@ -245,6 +245,40 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="ai-translator-request-timeout"><?php echo esc_html__( 'Request timeout', 'robotstxt-ai-translator' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="ai-translator-request-timeout"
|
||||
name="ai_translator_settings[request_timeout]"
|
||||
value="<?php echo esc_attr( (string) $current['request_timeout'] ); ?>"
|
||||
min="30"
|
||||
max="300"
|
||||
step="1"
|
||||
class="small-text"
|
||||
/>
|
||||
<span class="description"><?php echo esc_html__( 'seconds', 'robotstxt-ai-translator' ); ?></span>
|
||||
<p class="description">
|
||||
<?php
|
||||
$ini_raw = ini_get( 'max_execution_time' );
|
||||
$php_limit = is_string( $ini_raw ) ? (int) $ini_raw : 0;
|
||||
if ( $php_limit > 0 ) {
|
||||
printf(
|
||||
/* translators: %d: PHP max_execution_time in seconds. */
|
||||
esc_html__( 'Maximum time the server waits for an AI response. PHP max_execution_time is %d s — values above this have no effect.', 'robotstxt-ai-translator' ),
|
||||
absint( $php_limit )
|
||||
);
|
||||
} else {
|
||||
esc_html_e( 'Maximum time the server waits for an AI response. PHP max_execution_time is unlimited.', 'robotstxt-ai-translator' );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php if ( class_exists( '\Inpsyde\MultilingualPress\TranslationUi\Post\MetaboxAction' ) ) : ?>
|
||||
<tr>
|
||||
<th scope="row"><?php echo esc_html__( 'MultilingualPress', 'robotstxt-ai-translator' ); ?></th>
|
||||
|
|
@ -355,6 +389,40 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="ai-translator-net-request-timeout"><?php echo esc_html__( 'Request timeout', 'robotstxt-ai-translator' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
id="ai-translator-net-request-timeout"
|
||||
name="ai_translator_settings[request_timeout]"
|
||||
value="<?php echo esc_attr( (string) $network['request_timeout'] ); ?>"
|
||||
min="30"
|
||||
max="300"
|
||||
step="1"
|
||||
class="small-text"
|
||||
/>
|
||||
<span class="description"><?php echo esc_html__( 'seconds', 'robotstxt-ai-translator' ); ?></span>
|
||||
<p class="description">
|
||||
<?php
|
||||
$ini_raw_net = ini_get( 'max_execution_time' );
|
||||
$php_limit_net = is_string( $ini_raw_net ) ? (int) $ini_raw_net : 0;
|
||||
if ( $php_limit_net > 0 ) {
|
||||
printf(
|
||||
/* translators: %d: PHP max_execution_time in seconds. */
|
||||
esc_html__( 'Maximum time the server waits for an AI response. PHP max_execution_time is %d s — values above this have no effect.', 'robotstxt-ai-translator' ),
|
||||
absint( $php_limit_net )
|
||||
);
|
||||
} else {
|
||||
esc_html_e( 'Maximum time the server waits for an AI response. PHP max_execution_time is unlimited.', 'robotstxt-ai-translator' );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php if ( class_exists( '\Inpsyde\MultilingualPress\TranslationUi\Post\MetaboxAction' ) ) : ?>
|
||||
<tr>
|
||||
<th scope="row"><?php echo esc_html__( 'MultilingualPress', 'robotstxt-ai-translator' ); ?></th>
|
||||
|
|
@ -563,18 +631,18 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sanitises a raw settings array into a strict boolean schema.
|
||||
* Sanitises a raw settings array into a typed schema.
|
||||
*
|
||||
* This is the single sanitisation boundary for all form input from both the
|
||||
* site and network settings pages. Raw POST values arrive via wp_unslash()
|
||||
* and are converted here to strict booleans. Any new field added to this
|
||||
* schema MUST be sanitised in this method before being stored.
|
||||
* and are converted here to strict types. Any new field added to this schema
|
||||
* MUST be sanitised in this method before being stored.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param array<string,mixed> $raw Raw input from the form.
|
||||
*
|
||||
* @return array<string,bool>
|
||||
* @return array<string,bool|int>
|
||||
*/
|
||||
private function sanitize_settings( array $raw ) {
|
||||
return array(
|
||||
|
|
@ -582,6 +650,7 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
|
|||
'translate_content' => ! empty( $raw['translate_content'] ),
|
||||
'translate_excerpt' => ! empty( $raw['translate_excerpt'] ),
|
||||
'auto_translate_on_mlp_create' => ! empty( $raw['auto_translate_on_mlp_create'] ),
|
||||
'request_timeout' => max( 30, min( 300, isset( $raw['request_timeout'] ) && is_numeric( $raw['request_timeout'] ) ? (int) $raw['request_timeout'] : 60 ) ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,41 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
|
|||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
AI_TRANSLATOR_REST_NAMESPACE,
|
||||
'/translate-text',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'handle_translate_text_request' ),
|
||||
'permission_callback' => array( $this, 'check_translate_permissions' ),
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'minimum' => 1,
|
||||
'sanitize_callback' => 'absint',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
),
|
||||
'text' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'minLength' => 1,
|
||||
'maxLength' => 20000,
|
||||
'sanitize_callback' => array( $this, 'sanitize_raw_text' ),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
),
|
||||
'target_locale' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'pattern' => '^[A-Za-z]{2,3}(_[A-Za-z0-9]{2,8})?$',
|
||||
'maxLength' => 20,
|
||||
'sanitize_callback' => array( $this, 'sanitize_locale' ),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -141,6 +176,79 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
|
|||
return substr( $value, 0, 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitises a raw text value without stripping HTML.
|
||||
*
|
||||
* The /translate-text endpoint accepts HTML content (Gutenberg block markup,
|
||||
* Classic editor HTML, etc.). Standard WordPress text sanitisers strip HTML
|
||||
* tags; this method only casts to string and preserves markup intact.
|
||||
* Security is enforced by the permission_callback (requires edit_post capability).
|
||||
*
|
||||
* @since 1.2.0
|
||||
*
|
||||
* @param mixed $value Raw value from the request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sanitize_raw_text( $value ) {
|
||||
return is_string( $value ) ? $value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* REST callback for the translate-text endpoint.
|
||||
*
|
||||
* Translates a raw text string (HTML or plain text) into the target locale.
|
||||
* The post_id parameter is used solely for authorisation; no post content is
|
||||
* read from the database. This endpoint is designed for client-side chunked
|
||||
* translation where the caller supplies the text directly.
|
||||
*
|
||||
* @since 1.2.0
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function handle_translate_text_request( WP_REST_Request $request ) {
|
||||
$post_id_param = $request->get_param( 'post_id' );
|
||||
$text_param = $request->get_param( 'text' );
|
||||
$target_locale_param = $request->get_param( 'target_locale' );
|
||||
|
||||
$post_id = is_numeric( $post_id_param ) ? (int) $post_id_param : 0;
|
||||
$text = is_string( $text_param ) ? $text_param : '';
|
||||
$target_locale = is_string( $target_locale_param ) ? $target_locale_param : '';
|
||||
|
||||
if ( '' === trim( $text ) ) {
|
||||
return new WP_REST_Response( array( 'text' => '' ), 200 );
|
||||
}
|
||||
|
||||
// Verify the post exists (post_id is already authorised by the permission callback).
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post instanceof WP_Post ) {
|
||||
return new WP_Error(
|
||||
'ai_translator_post_not_found',
|
||||
__( 'Post not found.', 'robotstxt-ai-translator' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the locale is installed on this site.
|
||||
$installed = $this->translator->get_installed_locales();
|
||||
if ( ! in_array( $target_locale, $installed, true ) ) {
|
||||
return new WP_Error(
|
||||
'ai_translator_invalid_locale',
|
||||
__( 'The selected language is not installed on this site.', 'robotstxt-ai-translator' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$translated = $this->translator->translate( $text, $target_locale );
|
||||
if ( is_wp_error( $translated ) ) {
|
||||
return $this->error_with_status( $translated, 502 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( array( 'text' => $translated ), 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* REST permission callback for the translate endpoint.
|
||||
*
|
||||
|
|
@ -430,6 +538,8 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
|
|||
$data = array(
|
||||
'restNamespace' => AI_TRANSLATOR_REST_NAMESPACE,
|
||||
'restRoute' => '/translate',
|
||||
'chunkTextRoute' => '/translate-text',
|
||||
'chunkSize' => 5000,
|
||||
'context' => $context,
|
||||
'postId' => $post_id,
|
||||
'available' => $this->translator->is_available(),
|
||||
|
|
@ -442,6 +552,8 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
|
|||
'i18n' => array(
|
||||
'translate' => __( 'Translate', 'robotstxt-ai-translator' ),
|
||||
'translating' => __( 'Translating…', 'robotstxt-ai-translator' ),
|
||||
/* translators: 1: number of chunks translated so far, 2: total number of chunks. */
|
||||
'translatingProgress' => __( 'Translating… ({done}/{total})', 'robotstxt-ai-translator' ),
|
||||
'targetLanguage' => __( 'Target language', 'robotstxt-ai-translator' ),
|
||||
'panelTitle' => __( 'AI Translator', 'robotstxt-ai-translator' ),
|
||||
'success' => __( 'Translation applied. Review and save the post.', 'robotstxt-ai-translator' ),
|
||||
|
|
|
|||
|
|
@ -51,14 +51,17 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
|
|||
/**
|
||||
* Default settings values.
|
||||
*
|
||||
* Boolean keys are coerced to bool on read/write; integer keys to int.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @var array<string,bool>
|
||||
* @var array<string,bool|int>
|
||||
*/
|
||||
const DEFAULTS = array(
|
||||
'translate_title' => true,
|
||||
'translate_content' => true,
|
||||
'translate_excerpt' => true,
|
||||
'auto_translate_on_mlp_create' => false,
|
||||
'request_timeout' => 60,
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
@ -104,7 +107,7 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
|
|||
*
|
||||
* @param int|null $blog_id Optional blog ID. Defaults to the current site.
|
||||
*
|
||||
* @return array<string,bool> Settings array with boolean values.
|
||||
* @return array<string,bool|int> Settings array with typed values.
|
||||
*/
|
||||
public function get_settings( $blog_id = null ) {
|
||||
if ( ! is_multisite() ) {
|
||||
|
|
@ -139,7 +142,7 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return array<string,bool>
|
||||
* @return array<string,bool|int>
|
||||
*/
|
||||
public function get_network_settings() {
|
||||
return $this->normalize( (array) get_site_option( self::OPTION_NETWORK, array() ) );
|
||||
|
|
@ -150,7 +153,7 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return array<string,bool>
|
||||
* @return array<string,bool|int>
|
||||
*/
|
||||
public function get_site_settings() {
|
||||
return $this->normalize( (array) get_option( self::OPTION_SITE, array() ) );
|
||||
|
|
@ -161,7 +164,7 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param array<string,bool> $settings Settings to store.
|
||||
* @param array<string,bool|int> $settings Settings to store.
|
||||
*
|
||||
* @return bool True on success.
|
||||
*/
|
||||
|
|
@ -178,7 +181,7 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param array<string,bool> $settings Settings to store.
|
||||
* @param array<string,bool|int> $settings Settings to store.
|
||||
*
|
||||
* @return bool True on success.
|
||||
*/
|
||||
|
|
@ -187,22 +190,30 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Normalises raw settings into a strict array of booleans matching the schema.
|
||||
* Normalises raw settings into a typed array matching the schema.
|
||||
*
|
||||
* Boolean keys are coerced to strict bool; integer keys to strict int.
|
||||
* The DEFAULTS constant is the authoritative schema: any key not present
|
||||
* there is silently dropped, and any missing key falls back to its default.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param array<int|string,mixed> $settings Raw settings.
|
||||
*
|
||||
* @return array<string,bool>
|
||||
* @return array<string,bool|int>
|
||||
*/
|
||||
private function normalize( array $settings ) {
|
||||
$normalized = array();
|
||||
|
||||
foreach ( self::DEFAULTS as $key => $default ) {
|
||||
if ( array_key_exists( $key, $settings ) ) {
|
||||
$normalized[ $key ] = (bool) $settings[ $key ];
|
||||
if ( is_int( $default ) ) {
|
||||
$normalized[ $key ] = (int) $settings[ $key ];
|
||||
} else {
|
||||
$normalized[ $key ] = (bool) $default;
|
||||
$normalized[ $key ] = (bool) $settings[ $key ];
|
||||
}
|
||||
} else {
|
||||
$normalized[ $key ] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,52 @@ if ( ! class_exists( 'AI_Translator_Translator' ) ) {
|
|||
*/
|
||||
class AI_Translator_Translator {
|
||||
|
||||
/**
|
||||
* Settings handler.
|
||||
*
|
||||
* @since 1.2.0
|
||||
* @var AI_Translator_Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* Timeout value (seconds) enforced during an active translate() call.
|
||||
* Null when no call is in progress.
|
||||
*
|
||||
* @since 1.2.0
|
||||
* @var int|null
|
||||
*/
|
||||
private $active_timeout = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 1.2.0
|
||||
*
|
||||
* @param AI_Translator_Settings $settings Settings handler.
|
||||
*/
|
||||
public function __construct( AI_Translator_Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the http_request_timeout value while an AI translation call is
|
||||
* in progress. Registered and de-registered by translate() around each call.
|
||||
*
|
||||
* @since 1.2.0
|
||||
*
|
||||
* @param mixed $timeout Current timeout value passed by WordPress.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function filter_timeout( $timeout ) {
|
||||
if ( null !== $this->active_timeout ) {
|
||||
return $this->active_timeout;
|
||||
}
|
||||
|
||||
return is_numeric( $timeout ) ? (int) $timeout : 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether AI features are enabled for this WordPress installation.
|
||||
*
|
||||
|
|
@ -66,6 +112,9 @@ if ( ! class_exists( 'AI_Translator_Translator' ) ) {
|
|||
/**
|
||||
* Translates a single piece of text to the target locale.
|
||||
*
|
||||
* Applies the configured request_timeout via the http_request_timeout filter
|
||||
* for the duration of the AI call, then restores the previous timeout.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $text Source text. Plain text or HTML; preserved as-is.
|
||||
|
|
@ -109,16 +158,24 @@ if ( ! class_exists( 'AI_Translator_Translator' ) ) {
|
|||
$language_name
|
||||
);
|
||||
|
||||
// Apply the configured request timeout around the AI call.
|
||||
$site_settings = $this->settings->get_settings();
|
||||
$this->active_timeout = max( 30, (int) $site_settings['request_timeout'] );
|
||||
add_filter( 'http_request_timeout', array( $this, 'filter_timeout' ), PHP_INT_MAX );
|
||||
|
||||
try {
|
||||
$result = wp_ai_client_prompt( $text )
|
||||
->using_system_instruction( $system_instruction )
|
||||
->generate_text();
|
||||
} catch ( Exception $e ) {
|
||||
return new WP_Error( 'ai_translator_exception', $e->getMessage() );
|
||||
$result = new WP_Error( 'ai_translator_exception', $e->getMessage() );
|
||||
} catch ( Throwable $e ) {
|
||||
return new WP_Error( 'ai_translator_exception', $e->getMessage() );
|
||||
$result = new WP_Error( 'ai_translator_exception', $e->getMessage() );
|
||||
}
|
||||
|
||||
remove_filter( 'http_request_timeout', array( $this, 'filter_timeout' ), PHP_INT_MAX );
|
||||
$this->active_timeout = null;
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.1.0\n"
|
||||
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.2.0\n"
|
||||
"Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator\n"
|
||||
"Last-Translator: ROBOTSTXT <hello@robotstxt.es>\n"
|
||||
"Language-Team: Catalan\n"
|
||||
|
|
@ -407,3 +407,26 @@ msgstr "AI Translator (by ROBOTSTXT) requereix un proveïdor d'IA configurat per
|
|||
#: robotstxt-ai-translator.php:145
|
||||
msgid "Settings → AI"
|
||||
msgstr "Ajustos → IA"
|
||||
|
||||
#: includes/class-ai-translator-admin.php
|
||||
msgid "Request timeout"
|
||||
msgstr "Temps màxim de resposta"
|
||||
|
||||
#: includes/class-ai-translator-admin.php
|
||||
msgid "seconds"
|
||||
msgstr "segons"
|
||||
|
||||
#. translators: %d: PHP max_execution_time in seconds.
|
||||
#: includes/class-ai-translator-admin.php
|
||||
#, php-format
|
||||
msgid "Maximum time the server waits for an AI response. PHP max_execution_time is %d s — values above this have no effect."
|
||||
msgstr "Temps màxim que el servidor espera una resposta de la IA. El max_execution_time de PHP és de %d s — els valors superiors no tenen efecte."
|
||||
|
||||
#: includes/class-ai-translator-admin.php
|
||||
msgid "Maximum time the server waits for an AI response. PHP max_execution_time is unlimited."
|
||||
msgstr "Temps màxim que el servidor espera una resposta de la IA. El max_execution_time de PHP és il·limitat."
|
||||
|
||||
#. translators: 1: number of chunks translated so far, 2: total number of chunks.
|
||||
#: includes/class-ai-translator-editor-ui.php
|
||||
msgid "Translating… ({done}/{total})"
|
||||
msgstr "Traduint… ({done}/{total})"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.1.0\n"
|
||||
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.2.0\n"
|
||||
"Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator\n"
|
||||
"Last-Translator: ROBOTSTXT <hello@robotstxt.es>\n"
|
||||
"Language-Team: Spanish (Spain)\n"
|
||||
|
|
@ -407,3 +407,26 @@ msgstr "AI Translator (by ROBOTSTXT) requiere un proveedor de IA configurado par
|
|||
#: robotstxt-ai-translator.php:145
|
||||
msgid "Settings → AI"
|
||||
msgstr "Ajustes → IA"
|
||||
|
||||
#: includes/class-ai-translator-admin.php
|
||||
msgid "Request timeout"
|
||||
msgstr "Tiempo máximo de respuesta"
|
||||
|
||||
#: includes/class-ai-translator-admin.php
|
||||
msgid "seconds"
|
||||
msgstr "segundos"
|
||||
|
||||
#. translators: %d: PHP max_execution_time in seconds.
|
||||
#: includes/class-ai-translator-admin.php
|
||||
#, php-format
|
||||
msgid "Maximum time the server waits for an AI response. PHP max_execution_time is %d s — values above this have no effect."
|
||||
msgstr "Tiempo máximo que el servidor espera una respuesta de la IA. El max_execution_time de PHP es de %d s — los valores superiores no tienen efecto."
|
||||
|
||||
#: includes/class-ai-translator-admin.php
|
||||
msgid "Maximum time the server waits for an AI response. PHP max_execution_time is unlimited."
|
||||
msgstr "Tiempo máximo que el servidor espera una respuesta de la IA. El max_execution_time de PHP es ilimitado."
|
||||
|
||||
#. translators: 1: number of chunks translated so far, 2: total number of chunks.
|
||||
#: includes/class-ai-translator-editor-ui.php
|
||||
msgid "Translating… ({done}/{total})"
|
||||
msgstr "Traduciendo… ({done}/{total})"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
# This file is distributed under the GPL-3.0-or-later.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.1.0\n"
|
||||
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.2.0\n"
|
||||
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-ai-translator\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2026-05-25T16:53:01+00:00\n"
|
||||
"POT-Creation-Date: 2026-05-28T06:44:22+00:00\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"X-Generator: WP-CLI 2.12.0\n"
|
||||
"X-Domain: robotstxt-ai-translator\n"
|
||||
|
|
@ -51,8 +51,8 @@ msgstr ""
|
|||
#: includes/class-ai-translator-admin.php:113
|
||||
#: includes/class-ai-translator-admin.php:130
|
||||
#: includes/class-ai-translator-admin.php:131
|
||||
#: includes/class-ai-translator-editor-ui.php:294
|
||||
#: includes/class-ai-translator-editor-ui.php:446
|
||||
#: includes/class-ai-translator-editor-ui.php:402
|
||||
#: includes/class-ai-translator-editor-ui.php:558
|
||||
msgid "AI Translator"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ msgid "Settings"
|
|||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:187
|
||||
#: includes/class-ai-translator-admin.php:287
|
||||
#: includes/class-ai-translator-admin.php:321
|
||||
msgid "You do not have permission to access this page."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -76,26 +76,26 @@ msgstr ""
|
|||
|
||||
#: includes/class-ai-translator-admin.php:211
|
||||
#: includes/class-ai-translator-admin.php:214
|
||||
#: includes/class-ai-translator-admin.php:330
|
||||
#: includes/class-ai-translator-admin.php:334
|
||||
#: includes/class-ai-translator-admin.php:364
|
||||
#: includes/class-ai-translator-admin.php:368
|
||||
msgid "Translation fields"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:218
|
||||
#: includes/class-ai-translator-admin.php:338
|
||||
#: includes/class-ai-translator-editor-ui.php:453
|
||||
#: includes/class-ai-translator-admin.php:372
|
||||
#: includes/class-ai-translator-editor-ui.php:565
|
||||
msgid "Translate title"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:223
|
||||
#: includes/class-ai-translator-admin.php:343
|
||||
#: includes/class-ai-translator-editor-ui.php:454
|
||||
#: includes/class-ai-translator-admin.php:377
|
||||
#: includes/class-ai-translator-editor-ui.php:566
|
||||
msgid "Translate content"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:228
|
||||
#: includes/class-ai-translator-admin.php:348
|
||||
#: includes/class-ai-translator-editor-ui.php:455
|
||||
#: includes/class-ai-translator-admin.php:382
|
||||
#: includes/class-ai-translator-editor-ui.php:567
|
||||
msgid "Translate excerpt"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -118,169 +118,191 @@ msgid "disabled"
|
|||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:250
|
||||
#: includes/class-ai-translator-admin.php:253
|
||||
#: includes/class-ai-translator-admin.php:360
|
||||
#: includes/class-ai-translator-admin.php:363
|
||||
#: includes/class-ai-translator-admin.php:394
|
||||
msgid "Request timeout"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:263
|
||||
#: includes/class-ai-translator-admin.php:407
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
|
||||
#. translators: %d: PHP max_execution_time in seconds.
|
||||
#: includes/class-ai-translator-admin.php:271
|
||||
#: includes/class-ai-translator-admin.php:415
|
||||
#, php-format
|
||||
msgid "Maximum time the server waits for an AI response. PHP max_execution_time is %d s — values above this have no effect."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:275
|
||||
#: includes/class-ai-translator-admin.php:419
|
||||
msgid "Maximum time the server waits for an AI response. PHP max_execution_time is unlimited."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:284
|
||||
#: includes/class-ai-translator-admin.php:287
|
||||
#: includes/class-ai-translator-admin.php:428
|
||||
#: includes/class-ai-translator-admin.php:431
|
||||
msgid "MultilingualPress"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:257
|
||||
#: includes/class-ai-translator-admin.php:367
|
||||
#: includes/class-ai-translator-admin.php:291
|
||||
#: includes/class-ai-translator-admin.php:435
|
||||
msgid "Auto-translate new connected posts"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:261
|
||||
#: includes/class-ai-translator-admin.php:371
|
||||
#: includes/class-ai-translator-admin.php:295
|
||||
#: includes/class-ai-translator-admin.php:439
|
||||
msgid "When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site's language. The translation runs synchronously during save."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:296
|
||||
#: includes/class-ai-translator-admin.php:330
|
||||
msgid "AI Translator Network Settings"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:300
|
||||
#: includes/class-ai-translator-admin.php:334
|
||||
msgid "Network settings saved."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:310
|
||||
#: includes/class-ai-translator-admin.php:313
|
||||
#: includes/class-ai-translator-admin.php:344
|
||||
#: includes/class-ai-translator-admin.php:347
|
||||
msgid "Configuration mode"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:317
|
||||
#: includes/class-ai-translator-admin.php:351
|
||||
msgid "Global configuration: a single setting applies to every site."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:322
|
||||
#: includes/class-ai-translator-admin.php:356
|
||||
msgid "Per-site configuration: each site can override these defaults."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:352
|
||||
#: includes/class-ai-translator-admin.php:386
|
||||
msgid "In global mode this is the configuration for the whole network. In per-site mode these values are used as defaults for sites that have not been configured individually."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:397
|
||||
#: includes/class-ai-translator-admin.php:429
|
||||
#: includes/class-ai-translator-admin.php:465
|
||||
#: includes/class-ai-translator-admin.php:497
|
||||
msgid "You do not have permission to perform this action."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:473
|
||||
#: includes/class-ai-translator-admin.php:541
|
||||
msgid "Recommended models"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:476
|
||||
#: includes/class-ai-translator-admin.php:544
|
||||
msgid "This plugin does not call AI providers directly. Configure your preferred provider in the WordPress AI plugin settings. The table below is a guide to help you pick a model based on your translation use case."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:482
|
||||
#: includes/class-ai-translator-admin.php:550
|
||||
msgid "Use case"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:483
|
||||
#: includes/class-ai-translator-admin.php:551
|
||||
msgid "Recommended model"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:484
|
||||
#: includes/class-ai-translator-admin.php:552
|
||||
msgid "Why"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:499
|
||||
#: includes/class-ai-translator-admin.php:567
|
||||
msgid "These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:514
|
||||
#: includes/class-ai-translator-admin.php:582
|
||||
msgid "European languages (ES, CA, FR, DE, IT, PT)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:515
|
||||
#: includes/class-ai-translator-admin.php:583
|
||||
msgid "DeepL API Pro"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:516
|
||||
#: includes/class-ai-translator-admin.php:584
|
||||
msgid "Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:519
|
||||
#: includes/class-ai-translator-admin.php:587
|
||||
msgid "Asian languages (ZH, JA, KO)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:520
|
||||
#: includes/class-ai-translator-admin.php:588
|
||||
msgid "GPT-4o / GPT-5 or Claude Sonnet 4"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:521
|
||||
#: includes/class-ai-translator-admin.php:589
|
||||
msgid "Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:524
|
||||
#: includes/class-ai-translator-admin.php:592
|
||||
msgid "Marketing / brand tone"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:525
|
||||
#: includes/class-ai-translator-admin.php:593
|
||||
msgid "Claude Sonnet 4 / Opus 4"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:526
|
||||
#: includes/class-ai-translator-admin.php:594
|
||||
msgid "Better preservation of tone, brand voice, and nuance. Ideal for creative copy."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:529
|
||||
#: includes/class-ai-translator-admin.php:597
|
||||
msgid "Technical documentation / code"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:530
|
||||
#: includes/class-ai-translator-admin.php:598
|
||||
msgid "GPT-4o / GPT-5"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:531
|
||||
#: includes/class-ai-translator-admin.php:599
|
||||
msgid "Higher accuracy with variables, structured formats, and technical terminology."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:534
|
||||
#: includes/class-ai-translator-admin.php:602
|
||||
msgid "Long documents (100+ pages)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:535
|
||||
#: includes/class-ai-translator-admin.php:603
|
||||
msgid "Gemini 2.5 Pro"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:536
|
||||
#: includes/class-ai-translator-admin.php:604
|
||||
msgid "1M token context window. Maintains terminological consistency across long texts."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:539
|
||||
#: includes/class-ai-translator-admin.php:607
|
||||
msgid "High volume / low cost"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:540
|
||||
#: includes/class-ai-translator-admin.php:608
|
||||
msgid "DeepSeek-V3"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:541
|
||||
#: includes/class-ai-translator-admin.php:609
|
||||
msgid "Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT)."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:544
|
||||
#: includes/class-ai-translator-admin.php:612
|
||||
msgid "Rare / indigenous languages"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:545
|
||||
#: includes/class-ai-translator-admin.php:613
|
||||
msgid "Claude Sonnet or Taskade Translate (multi-model routing)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:546
|
||||
#: includes/class-ai-translator-admin.php:614
|
||||
msgid "Better coverage for uncommon language pairs."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:549
|
||||
#: includes/class-ai-translator-admin.php:617
|
||||
msgid "Maximum coverage (133+ languages)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:550
|
||||
#: includes/class-ai-translator-admin.php:618
|
||||
msgid "Google Cloud Translation"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-admin.php:551
|
||||
#: includes/class-ai-translator-admin.php:619
|
||||
msgid "The most complete option, though with slightly lower quality on European languages."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -303,14 +325,14 @@ msgid "No user context. Pass --user=<id|login> so post updates carry an author a
|
|||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-cli.php:145
|
||||
#: includes/class-ai-translator-editor-ui.php:319
|
||||
#: includes/class-ai-translator-editor-ui.php:451
|
||||
#: includes/class-ai-translator-translator.php:86
|
||||
#: includes/class-ai-translator-editor-ui.php:427
|
||||
#: includes/class-ai-translator-editor-ui.php:563
|
||||
#: includes/class-ai-translator-translator.php:135
|
||||
msgid "AI features are disabled for this WordPress installation."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-cli.php:149
|
||||
#: includes/class-ai-translator-translator.php:93
|
||||
#: includes/class-ai-translator-translator.php:142
|
||||
msgid "No AI provider is configured for text generation. Open Settings → AI to set one up."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -328,73 +350,80 @@ msgstr ""
|
|||
msgid "%1$d translated, %2$d failed (locale: %3$s)."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:160
|
||||
msgid "Sorry, you are not allowed to edit this post."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:190
|
||||
#: includes/class-ai-translator-editor-ui.php:229
|
||||
#: includes/class-ai-translator-editor-ui.php:298
|
||||
msgid "Post not found."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:199
|
||||
#: includes/class-ai-translator-editor-ui.php:239
|
||||
#: includes/class-ai-translator-editor-ui.php:307
|
||||
msgid "The selected language is not installed on this site."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:221
|
||||
#: includes/class-ai-translator-editor-ui.php:268
|
||||
msgid "Sorry, you are not allowed to edit this post."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:329
|
||||
msgid "No translatable fields were selected, or the requested fields are disabled in the settings."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:321
|
||||
#: includes/class-ai-translator-editor-ui.php:450
|
||||
#: includes/class-ai-translator-editor-ui.php:429
|
||||
#: includes/class-ai-translator-editor-ui.php:562
|
||||
msgid "No translation fields are enabled in settings."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:323
|
||||
#: includes/class-ai-translator-editor-ui.php:449
|
||||
#: includes/class-ai-translator-editor-ui.php:431
|
||||
#: includes/class-ai-translator-editor-ui.php:561
|
||||
msgid "No languages are installed on this site."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:326
|
||||
#: includes/class-ai-translator-editor-ui.php:445
|
||||
#: includes/class-ai-translator-editor-ui.php:434
|
||||
#: includes/class-ai-translator-editor-ui.php:557
|
||||
msgid "Target language"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:336
|
||||
#: includes/class-ai-translator-editor-ui.php:443
|
||||
#: includes/class-ai-translator-editor-ui.php:444
|
||||
#: includes/class-ai-translator-editor-ui.php:553
|
||||
msgid "Translate"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:341
|
||||
#: includes/class-ai-translator-editor-ui.php:449
|
||||
msgid "Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:444
|
||||
#: includes/class-ai-translator-editor-ui.php:554
|
||||
msgid "Translating…"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:447
|
||||
#. translators: 1: number of chunks translated so far, 2: total number of chunks.
|
||||
#: includes/class-ai-translator-editor-ui.php:556
|
||||
msgid "Translating… ({done}/{total})"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:559
|
||||
msgid "Translation applied. Review and save the post."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:448
|
||||
#: includes/class-ai-translator-editor-ui.php:560
|
||||
msgid "Translation failed."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-editor-ui.php:452
|
||||
#: includes/class-ai-translator-editor-ui.php:564
|
||||
msgid "You have unsaved changes. Save the post before translating to ensure the latest content is used."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-translator.php:102
|
||||
#: includes/class-ai-translator-translator.php:151
|
||||
msgid "The target language is not installed on this site."
|
||||
msgstr ""
|
||||
|
||||
#. translators: %s: target language name.
|
||||
#: includes/class-ai-translator-translator.php:108
|
||||
#: includes/class-ai-translator-translator.php:157
|
||||
#, php-format
|
||||
msgid "You are a professional translator. Translate the user message into %s. Preserve any HTML, shortcodes, line breaks, and Markdown exactly as they appear. Return only the translated text, without explanations, prefaces, or quotation marks."
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-ai-translator-translator.php:129
|
||||
#: includes/class-ai-translator-translator.php:186
|
||||
msgid "The AI service returned an unexpected response."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
10
readme.txt
10
readme.txt
|
|
@ -4,7 +4,7 @@ Tags: ai, translation, multilingual, multisite, editor
|
|||
Requires at least: 7.0
|
||||
Tested up to: 7.1
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.1.0
|
||||
Stable tag: 1.2.0
|
||||
License: GPL-3.0-or-later
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
|
|
@ -140,6 +140,14 @@ The plugin reads the post title and content from the database, not from the edit
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 1.2.0 =
|
||||
|
||||
* Added configurable "Request timeout" setting (default 60 s, minimum 30 s, maximum 300 s) in site and network admin pages.
|
||||
* Added parallel chunked content translation: long posts are split at Gutenberg block or paragraph boundaries and all chunks are sent to the AI concurrently, eliminating timeout errors on lengthy content.
|
||||
* Added real-time progress indicator on the Translate button: "Translating… (2/5)" as each chunk resolves.
|
||||
* Added new REST endpoint `POST /wp-json/ai-translator/v1/translate-text` for raw-text translation; used internally by the chunked content path.
|
||||
* Changed: content translation now reads from the current editor state (unsaved changes included); title and excerpt continue to read from the saved post.
|
||||
|
||||
= 1.1.0 =
|
||||
|
||||
* Added excerpt field (`post_excerpt`) as a translatable field, with a dedicated settings toggle.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator
|
||||
* Gitea Plugin URI: ROBOTSTXT/robotstxt-ai-translator
|
||||
* Description: Translate post titles and content from the editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration.
|
||||
* Version: 1.1.0
|
||||
* Version: 1.2.0
|
||||
* Author: ROBOTSTXT
|
||||
* Author URI: https://www.robotstxt.es/
|
||||
* License: GPL-3.0-or-later
|
||||
|
|
@ -25,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
/**
|
||||
* Plugin version.
|
||||
*/
|
||||
define( 'AI_TRANSLATOR_VERSION', '1.1.0' );
|
||||
define( 'AI_TRANSLATOR_VERSION', '1.2.0' );
|
||||
|
||||
/**
|
||||
* Main plugin file path.
|
||||
|
|
@ -94,7 +94,7 @@ add_action( 'init', 'ai_translator_load_textdomain' );
|
|||
*/
|
||||
function ai_translator_bootstrap() {
|
||||
$settings = new AI_Translator_Settings();
|
||||
$translator = new AI_Translator_Translator();
|
||||
$translator = new AI_Translator_Translator( $settings );
|
||||
$admin = new AI_Translator_Admin( $settings );
|
||||
$editor_ui = new AI_Translator_Editor_UI( $settings, $translator );
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ function ai_translator_dependency_notice() {
|
|||
return;
|
||||
}
|
||||
|
||||
$translator = new AI_Translator_Translator();
|
||||
$translator = new AI_Translator_Translator( new AI_Translator_Settings() );
|
||||
|
||||
if ( $translator->is_supported() ) {
|
||||
return;
|
||||
|
|
|
|||
10
update.json
10
update.json
|
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"name": "AI Translator (by ROBOTSTXT)",
|
||||
"slug": "robotstxt-ai-translator",
|
||||
"version": "1.1.0",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator/releases/download/1.1.0/robotstxt-ai-translator-1.1.0.zip",
|
||||
"version": "1.2.0",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator/releases/download/1.2.0/robotstxt-ai-translator-1.2.0.zip",
|
||||
"requires": "7.0",
|
||||
"requires_php": "7.4",
|
||||
"tested": "7.1",
|
||||
"last_updated": "2026-05-25",
|
||||
"last_updated": "2026-05-26",
|
||||
"author": "ROBOTSTXT",
|
||||
"author_profile": "https://www.robotstxt.es/",
|
||||
"homepage": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator",
|
||||
"description": "Translate post titles and content from the WordPress editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration.",
|
||||
"changelog": "<h3>1.1.0 - 2026-05-25</h3><ul><li><strong>Added:</strong> Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.</li><li><strong>Added:</strong> MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).</li><li><strong>Changed:</strong> Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.</li><li><strong>Changed:</strong> Admin notice now fires when no text-generation provider is configured.</li></ul><h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>",
|
||||
"changelog": "<h3>1.2.0 - 2026-05-26</h3><ul><li><strong>Added:</strong> Configurable request timeout — new \"Request timeout\" setting (default 60 s, min 30 s, max 300 s) controls how long the server waits for an AI response before timing out.</li><li><strong>Added:</strong> Parallel chunked content translation — long post content is automatically split at block (Gutenberg) or paragraph (Classic editor) boundaries and translated in parallel, eliminating timeout errors on lengthy posts.</li><li><strong>Added:</strong> Progress indicator — the Translate button shows \"Translating… (2/5)\" while chunks are in flight.</li><li><strong>Added:</strong> New REST endpoint POST /wp-json/ai-translator/v1/translate-text for raw-text translation; used internally by the chunked content path.</li></ul><h3>1.1.0 - 2026-05-25</h3><ul><li><strong>Added:</strong> Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.</li><li><strong>Added:</strong> MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).</li><li><strong>Changed:</strong> Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.</li><li><strong>Changed:</strong> Admin notice now fires when no text-generation provider is configured.</li></ul><h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>",
|
||||
"sections": {
|
||||
"description": "Translate post titles and content from the WordPress editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration.",
|
||||
"changelog": "<h3>1.1.0 - 2026-05-25</h3><ul><li><strong>Added:</strong> Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.</li><li><strong>Added:</strong> MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).</li><li><strong>Changed:</strong> Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.</li><li><strong>Changed:</strong> Admin notice now fires when no text-generation provider is configured.</li></ul><h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>"
|
||||
"changelog": "<h3>1.2.0 - 2026-05-26</h3><ul><li><strong>Added:</strong> Configurable request timeout — new \"Request timeout\" setting (default 60 s, min 30 s, max 300 s) controls how long the server waits for an AI response before timing out.</li><li><strong>Added:</strong> Parallel chunked content translation — long post content is automatically split at block (Gutenberg) or paragraph (Classic editor) boundaries and translated in parallel, eliminating timeout errors on lengthy posts.</li><li><strong>Added:</strong> Progress indicator — the Translate button shows \"Translating… (2/5)\" while chunks are in flight.</li><li><strong>Added:</strong> New REST endpoint POST /wp-json/ai-translator/v1/translate-text for raw-text translation; used internally by the chunked content path.</li></ul><h3>1.1.0 - 2026-05-25</h3><ul><li><strong>Added:</strong> Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.</li><li><strong>Added:</strong> MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).</li><li><strong>Changed:</strong> Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.</li><li><strong>Changed:</strong> Admin notice now fires when no text-generation provider is configured.</li></ul><h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>"
|
||||
},
|
||||
"banners": {
|
||||
"low": "",
|
||||
|
|
|
|||
Loading…
Reference in a new issue