$overrides Optional title / content / excerpt (translated in the editor). * @param bool $translate_meta Also machine-translate SEO fields (always on in "machine" mode). * @return int|WP_Error New post ID. */ public function create( int $source_id, string $lang, string $mode = self::MODE_COPY, array $overrides = array(), bool $translate_meta = false ): int|WP_Error { $source = get_post( $source_id ); $settings = $this->settings->get(); if ( ! $source ) { return new WP_Error( 'blocklingo_no_post', __( 'Source post not found.', 'blocklingo' ) ); } if ( ! $settings->is_language( $lang ) ) { return new WP_Error( 'blocklingo_bad_lang', __( 'Unknown language.', 'blocklingo' ) ); } if ( $this->posts->in_language( $source_id, $lang ) ) { return new WP_Error( 'blocklingo_exists', __( 'This translation already exists.', 'blocklingo' ) ); } $texts = $this->texts( $source, $lang, $mode ); if ( is_wp_error( $texts ) ) { return $texts; } // SEO texts are translated before anything is created, so a provider error leaves no half-made post. $meta_values = array(); $seo = (array) $settings->get( 'seo', array() ); if ( ( self::MODE_MACHINE === $mode || $translate_meta ) && ! empty( $seo['translate'] ) ) { $meta_values = $this->meta->translate_values( $source_id, $lang ); if ( is_wp_error( $meta_values ) ) { return $meta_values; } } $texts = array_merge( $texts, array_intersect_key( $overrides, $texts ) ); // Our own copying must not be pushed back to the source by the sync hooks. return $this->sync->paused( fn(): int|WP_Error => $this->insert( $source, $lang, $mode, $texts, $meta_values ) ); } /** * Title, content and excerpt for the new post. * * @return array{title: string, content: string, excerpt: string}|WP_Error */ private function texts( WP_Post $source, string $lang, string $mode ): array|WP_Error { if ( self::MODE_MACHINE === $mode ) { if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- not allowed on some hosts. } return $this->translator->translate_post( $source, $this->posts->language_of( $source->ID ), $lang ); } return array( 'title' => $source->post_title, 'content' => self::MODE_EMPTY === $mode ? '' : $source->post_content, 'excerpt' => self::MODE_EMPTY === $mode ? '' : $source->post_excerpt, ); } /** * @param array{title: string, content: string, excerpt: string} $texts * @param array $meta_values */ private function insert( WP_Post $source, string $lang, string $mode, array $texts, array $meta_values ): int|WP_Error { $user_id = get_current_user_id(); $new_id = wp_insert_post( wp_slash( array( 'post_type' => $source->post_type, 'post_status' => 'draft', 'post_title' => $texts['title'], 'post_content' => $this->pattern_refs->localize( $texts['content'], $lang ), 'post_excerpt' => $texts['excerpt'], 'post_author' => $user_id ? $user_id : $source->post_author, // Parent: the parent's translation in the same language, when there is one. 'post_parent' => $source->post_parent ? $this->posts->in_language( (int) $source->post_parent, $lang ) : 0, 'menu_order' => $source->menu_order, 'comment_status' => $source->comment_status, 'ping_status' => $source->ping_status, 'post_password' => $source->post_password, ) ), true ); if ( is_wp_error( $new_id ) ) { return $new_id; } // The insert tagged it with the default language; move it into the source's group. $row = $this->translations->find( $source->ID ); $group = $row ? (int) $row['group_id'] : $source->ID; if ( ! $row ) { $this->translations->assign( $source->ID, $this->settings->get()->default_code(), $group ); } $linked = $this->translations->assign( $new_id, $lang, $group ); if ( is_wp_error( $linked ) ) { wp_delete_post( $new_id, true ); return $linked; } $this->copy_meta( $source->ID, $new_id ); $this->copy_terms( $source, $new_id, $lang ); $this->meta->clear_per_language( $new_id ); if ( $meta_values ) { $this->meta->apply( $new_id, $meta_values ); } /** * A translation was created. * * @param int $new_id The new post. * @param int $source_id The post it was made from. * @param string $lang Its language. * @param string $mode copy | machine | empty. */ do_action( 'blocklingo_translation_created', $new_id, $source->ID, $lang, $mode ); return $new_id; } /** @return string[] Meta keys never copied; filter blocklingo_skip_meta_keys. */ private function skip_meta(): array { return (array) apply_filters( 'blocklingo_skip_meta_keys', array( '_edit_lock', '_edit_last', '_wp_old_slug', '_wp_old_date', '_encloseme', '_pingme', '_wp_trash_meta_status', '_wp_trash_meta_time' ) ); } private function copy_meta( int $from, int $to ): void { $skip = $this->skip_meta(); foreach ( (array) get_post_meta( $from ) as $key => $values ) { $key = (string) $key; if ( in_array( $key, $skip, true ) || str_starts_with( $key, '_blocklingo_' ) ) { continue; } delete_post_meta( $to, $key ); foreach ( (array) $values as $value ) { add_post_meta( $to, $key, wp_slash( maybe_unserialize( $value ) ) ); } } } /** Taxonomy terms as they are (term translation is not supported yet). */ /** The source's terms; in translated taxonomies their version in $lang, when there is one. */ private function copy_terms( WP_Post $source, int $to, string $lang ): void { foreach ( get_object_taxonomies( $source->post_type ) as $tax ) { $ids = wp_get_object_terms( $source->ID, $tax, array( 'fields' => 'ids' ) ); if ( ! is_wp_error( $ids ) && $ids ) { $ids = array_map( 'intval', $ids ); wp_set_object_terms( $to, $this->terms->is_translatable( $tax ) ? $this->terms->map( $ids, $lang ) : $ids, $tax ); } } } }