name, $from->code, $to->name, $to->code ); $p .= "Rules:\n"; $p .= "- Write as if the text had been written in {$to->name} from the start: natural and idiomatic, same meaning and intent. Adapt idioms, wordplay and rhythm; never translate word for word.\n"; $p .= "- Translate ordinary words. Leave in the original only brand and product names, names of technologies and frameworks, and terms that {$to->name} speakers in this field normally keep untranslated.\n"; $p .= "- Keep about the same length. Buttons, menu items and labels stay short. Keep the letter case style: UPPERCASE stays uppercase.\n"; $p .= "- Be consistent: the same source term gets the same translation in every item.\n"; $p .= "- Items are HTML fragments. Keep every tag, attribute, entity, URL, e-mail, @handle, shortcode and placeholder (like {year}, {{site.title}}, %%title%% or %sep%) exactly as is; translate only human-readable text.\n"; $p .= "- Translate each item on its own, even if it is one word or repeats another item. Never merge, split, explain or skip items. An item that needs no translation is returned unchanged.\n"; if ( $config->glossary ) { $p .= '- Never translate these terms, keep them exactly: ' . implode( ', ', $config->glossary ) . "\n"; } if ( '' !== trim( $context->style ) ) { $p .= "Style guide for {$to->name} (follow it strictly):\n" . trim( $context->style ) . "\n"; } if ( '' !== trim( $config->instructions ) ) { $p .= '- Extra instructions: ' . trim( $config->instructions ) . "\n"; } $p .= 'Input is {"items": {"": "", ...}, "notes": {"": ""}}. Respond with JSON only: {"translations": {"": "", ...}} with exactly the same ids.'; return $p . $this->page( $context ); } /** * User message: the items by id, and their notes (empty ones left out). * * @param array $items id => text, ids 1..n within the batch (PHP stores "1" as int 1). * @param string[] $notes One note per batch position (id - 1). */ public function user( array $items, array $notes ): string { $payload = array( 'items' => (object) $items ); $by_id = array(); foreach ( array_keys( $items ) as $id ) { $note = trim( (string) ( $notes[ (int) $id - 1 ] ?? '' ) ); if ( '' !== $note ) { $by_id[ (string) $id ] = $note; } } if ( $by_id ) { $payload['notes'] = (object) $by_id; } return (string) wp_json_encode( $payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); } /** * The model's answer as id => text (or a plain list). Accepts code fences * and chatty text around the JSON. * * @return array|WP_Error */ public function parse( string $text, string $label ): array|WP_Error { $text = trim( $text ); $text = (string) preg_replace( '/^```(?:json)?\s*|\s*```$/i', '', $text ); $data = json_decode( $text, true ); if ( ! is_array( $data ) ) { $start = strpos( $text, '{' ); $end = strrpos( $text, '}' ); if ( false !== $start && false !== $end ) { $data = json_decode( substr( $text, $start, $end - $start + 1 ), true ); } } if ( is_array( $data ) && isset( $data['translations'] ) && is_array( $data['translations'] ) ) { return $data['translations']; } // A bare list, or a bare object keyed by item ids ({"1": "...", "2": "..."}). if ( is_array( $data ) && $data && count( array_filter( array_keys( $data ), 'is_int' ) ) === count( $data ) ) { return $data; } return new WP_Error( 'blocklingo_mt_json', $label . ' returned text that is not the expected JSON.' ); } /** Page context, so short labels are translated in context. Empty when there is none. */ private function page( Context $c ): string { if ( ! $c->has_page() ) { return ''; } $p = "\n\nContext (for understanding only, do not translate this part):\n"; if ( '' !== $c->site ) { $p .= '- Website: ' . $c->site . "\n"; } if ( '' !== $c->title ) { $p .= '- ' . ( '' !== $c->type ? ucfirst( $c->type ) : 'Page' ) . ': "' . $c->title . "\"\n"; } $p .= "- Items are in page order; neighbouring items belong together (a heading, its text, its button). Notes say where an item appears: keep buttons, menu links and labels as short as the original.\n"; if ( '' !== $c->outline ) { $p .= "- Full page text in reading order:\n<<<\n" . $c->outline . "\n>>>\n"; } return $p; } }