register( 'pll.languages.list', [ 'class' => 'discovery', 'required_scopes' => [ 'read.wp', $scopes ], 'dependency' => $dep, 'description' => 'List all registered Polylang languages.', 'input_schema' => [], 'handler' => [ $this, 'handle_languages_list' ], ] ); $this->register( 'pll.post.get_language', [ 'class' => 'read', 'required_scopes' => [ 'read.wp', $scopes ], 'dependency' => $dep, 'description' => 'Get the language of a post.', 'input_schema' => [ 'post_id' => [ 'type' => 'integer', 'required' => true ], ], 'handler' => [ $this, 'handle_post_get_language' ], ] ); $this->register( 'pll.post.set_language', [ 'class' => 'write', 'required_scopes' => [ 'write.wp.posts', $scopes ], 'dependency' => $dep, 'description' => 'Set the language of a post.', 'input_schema' => [ 'post_id' => [ 'type' => 'integer', 'required' => true ], 'language' => [ 'type' => 'string', 'required' => true, 'description' => 'Language slug (e.g. "en", "ro").' ], ], 'handler' => [ $this, 'handle_post_set_language' ], ] ); $this->register( 'pll.post.get_translations', [ 'class' => 'read', 'required_scopes' => [ 'read.wp', $scopes ], 'dependency' => $dep, 'description' => 'Get all translations linked to a post.', 'input_schema' => [ 'post_id' => [ 'type' => 'integer', 'required' => true ], ], 'handler' => [ $this, 'handle_post_get_translations' ], ] ); $this->register( 'pll.post.link_translation', [ 'class' => 'admin_sensitive', 'required_scopes' => [ 'write.wp.posts', $scopes ], 'dependency' => $dep, 'description' => 'Link posts as translations. Pass translations as {"en": 123, "ro": 456, "uk": 789}. Existing group members not mentioned are preserved.', 'input_schema' => [ 'translations' => [ 'type' => 'object', 'required' => true, 'description' => 'Map of language_slug => post_id for all translations to link.' ], 'confirm' => [ 'type' => 'boolean', 'required' => true, 'description' => 'Must be true to execute.' ], ], 'handler' => [ $this, 'handle_post_link_translation' ], ] ); $this->register( 'pll.post.unlink_translation', [ 'class' => 'admin_sensitive', 'required_scopes' => [ 'write.wp.posts', $scopes ], 'dependency' => $dep, 'description' => 'Unlink a post from its translations group (confirm=true required).', 'input_schema' => [ 'post_id' => [ 'type' => 'integer', 'required' => true ], 'confirm' => [ 'type' => 'boolean', 'required' => true, 'description' => 'Must be true to execute.' ], ], 'handler' => [ $this, 'handle_post_unlink_translation' ], ] ); $this->register( 'pll.term.get_language', [ 'class' => 'read', 'required_scopes' => [ 'read.wp', $scopes ], 'dependency' => $dep, 'description' => 'Get the language of a term.', 'input_schema' => [ 'term_id' => [ 'type' => 'integer', 'required' => true ], ], 'handler' => [ $this, 'handle_term_get_language' ], ] ); $this->register( 'pll.term.set_language', [ 'class' => 'write', 'required_scopes' => [ 'manage.taxonomies', $scopes ], 'dependency' => $dep, 'description' => 'Set the language of a term.', 'input_schema' => [ 'term_id' => [ 'type' => 'integer', 'required' => true ], 'language' => [ 'type' => 'string', 'required' => true, 'description' => 'Language slug (e.g. "en", "ro").' ], ], 'handler' => [ $this, 'handle_term_set_language' ], ] ); $this->register( 'pll.term.get_translations', [ 'class' => 'read', 'required_scopes' => [ 'read.wp', $scopes ], 'dependency' => $dep, 'description' => 'Get all term translations.', 'input_schema' => [ 'term_id' => [ 'type' => 'integer', 'required' => true ], ], 'handler' => [ $this, 'handle_term_get_translations' ], ] ); $this->register( 'pll.term.link_translation', [ 'class' => 'admin_sensitive', 'required_scopes' => [ 'manage.taxonomies', $scopes ], 'dependency' => $dep, 'description' => 'Link terms as translations. Pass translations as {"en": 5, "ro": 8, "uk": 11}. Existing group members not mentioned are preserved.', 'input_schema' => [ 'translations' => [ 'type' => 'object', 'required' => true, 'description' => 'Map of language_slug => term_id for all translations to link.' ], 'confirm' => [ 'type' => 'boolean', 'required' => true, 'description' => 'Must be true to execute.' ], ], 'handler' => [ $this, 'handle_term_link_translation' ], ] ); } // ── Handlers ────────────────────────────────────────────────────────── public function handle_languages_list( array $args, array $key_record, bool $dry_run ): array { $languages = pll_languages_list( [ 'fields' => false ] ); $result = []; foreach ( $languages as $lang ) { $result[] = [ 'slug' => $lang->slug, 'name' => $lang->name, 'locale' => $lang->locale, 'flag' => $lang->flag_url ?? '', 'default' => ( function_exists( 'pll_default_language' ) && pll_default_language() === $lang->slug ), ]; } return $this->ok( [ 'languages' => $result ] ); } public function handle_post_get_language( array $args, array $key_record, bool $dry_run ): array { $post_id = $this->require_int( $args, 'post_id' ); if ( ! $post_id ) { return $this->err( 'MISSING_PARAM', 'Parameter post_id is required', 'validation_failed' ); } $lang = function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post_id ) : null; return $this->ok( [ 'post_id' => $post_id, 'language' => $lang ] ); } public function handle_post_set_language( array $args, array $key_record, bool $dry_run ): array { $post_id = $this->require_int( $args, 'post_id' ); $lang = sanitize_key( $args['language'] ?? '' ); if ( ! $post_id || ! $lang ) { return $this->err( 'MISSING_PARAM', 'Parameters post_id and language are required', 'validation_failed' ); } if ( ! AICOM_Policy_Engine::check_language_allowlist( $key_record, $lang ) ) { return $this->err( 'DENIED_ALLOWLIST', "Language not in allowlist: $lang", 'denied_allowlist', 403 ); } if ( $dry_run ) { return $this->ok( [ 'dry_run' => true, 'would_set_language' => $lang ] ); } if ( function_exists( 'pll_set_post_language' ) ) { pll_set_post_language( $post_id, $lang ); } return $this->ok( [ 'post_id' => $post_id, 'language' => $lang, 'updated' => true ], [ 'target_type' => 'post', 'target_id' => $post_id ] ); } public function handle_post_get_translations( array $args, array $key_record, bool $dry_run ): array { $post_id = $this->require_int( $args, 'post_id' ); if ( ! $post_id ) { return $this->err( 'MISSING_PARAM', 'Parameter post_id is required', 'validation_failed' ); } $translations = function_exists( 'pll_get_post_translations' ) ? pll_get_post_translations( $post_id ) : []; return $this->ok( [ 'post_id' => $post_id, 'translations' => $translations ] ); } public function handle_post_link_translation( array $args, array $key_record, bool $dry_run ): array { $translations = $args['translations'] ?? null; if ( ! is_array( $translations ) || empty( $translations ) ) { return $this->err( 'MISSING_PARAM', 'Parameter translations must be an object mapping language slugs to post IDs.', 'validation_failed' ); } // Sanitize and validate each entry $new_map = []; foreach ( $translations as $lang => $post_id ) { $lang = sanitize_key( $lang ); $post_id = (int) $post_id; if ( ! $lang || ! $post_id ) { return $this->err( 'INVALID_PARAM', "Invalid entry in translations: lang=$lang post_id=$post_id", 'validation_failed' ); } $new_map[ $lang ] = $post_id; } if ( $dry_run ) { return $this->ok( [ 'dry_run' => true, 'would_link' => $new_map ] ); } // Merge with existing translation groups so we never drop languages not mentioned $merged = $new_map; if ( function_exists( 'pll_get_post_translations' ) ) { foreach ( $new_map as $post_id ) { $existing = pll_get_post_translations( $post_id ); foreach ( $existing as $existing_lang => $existing_id ) { // New map takes precedence; only add languages not already specified if ( ! isset( $merged[ $existing_lang ] ) ) { $merged[ $existing_lang ] = $existing_id; } } } } if ( function_exists( 'pll_save_post_translations' ) ) { pll_save_post_translations( $merged ); } return $this->ok( [ 'linked' => true, 'group' => $merged ], [ 'target_type' => 'pll_translation', 'target_id' => implode( '-', array_values( $merged ) ) ] ); } public function handle_post_unlink_translation( array $args, array $key_record, bool $dry_run ): array { $post_id = $this->require_int( $args, 'post_id' ); if ( ! $post_id ) { return $this->err( 'MISSING_PARAM', 'Parameter post_id is required', 'validation_failed' ); } if ( $dry_run ) { return $this->ok( [ 'dry_run' => true, 'would_unlink_post_id' => $post_id ] ); } // Remove from translation group by saving only itself if ( function_exists( 'pll_get_post_language' ) && function_exists( 'pll_save_post_translations' ) ) { $lang = pll_get_post_language( $post_id ); if ( $lang ) { pll_save_post_translations( [ $lang => $post_id ] ); } } return $this->ok( [ 'post_id' => $post_id, 'unlinked' => true ], [ 'target_type' => 'post', 'target_id' => $post_id ] ); } public function handle_term_get_language( array $args, array $key_record, bool $dry_run ): array { $term_id = $this->require_int( $args, 'term_id' ); if ( ! $term_id ) { return $this->err( 'MISSING_PARAM', 'Parameter term_id is required', 'validation_failed' ); } $lang = function_exists( 'pll_get_term_language' ) ? pll_get_term_language( $term_id ) : null; return $this->ok( [ 'term_id' => $term_id, 'language' => $lang ] ); } public function handle_term_set_language( array $args, array $key_record, bool $dry_run ): array { $term_id = $this->require_int( $args, 'term_id' ); $lang = sanitize_key( $args['language'] ?? '' ); if ( ! $term_id || ! $lang ) { return $this->err( 'MISSING_PARAM', 'Parameters term_id and language are required', 'validation_failed' ); } if ( ! AICOM_Policy_Engine::check_language_allowlist( $key_record, $lang ) ) { return $this->err( 'DENIED_ALLOWLIST', "Language not in allowlist: $lang", 'denied_allowlist', 403 ); } if ( $dry_run ) { return $this->ok( [ 'dry_run' => true, 'would_set_language' => $lang ] ); } if ( function_exists( 'pll_set_term_language' ) ) { pll_set_term_language( $term_id, $lang ); } return $this->ok( [ 'term_id' => $term_id, 'language' => $lang, 'updated' => true ], [ 'target_type' => 'term', 'target_id' => $term_id ] ); } public function handle_term_get_translations( array $args, array $key_record, bool $dry_run ): array { $term_id = $this->require_int( $args, 'term_id' ); if ( ! $term_id ) { return $this->err( 'MISSING_PARAM', 'Parameter term_id is required', 'validation_failed' ); } $translations = function_exists( 'pll_get_term_translations' ) ? pll_get_term_translations( $term_id ) : []; return $this->ok( [ 'term_id' => $term_id, 'translations' => $translations ] ); } public function handle_term_link_translation( array $args, array $key_record, bool $dry_run ): array { $translations = $args['translations'] ?? null; if ( ! is_array( $translations ) || empty( $translations ) ) { return $this->err( 'MISSING_PARAM', 'Parameter translations must be an object mapping language slugs to term IDs.', 'validation_failed' ); } $new_map = []; foreach ( $translations as $lang => $term_id ) { $lang = sanitize_key( $lang ); $term_id = (int) $term_id; if ( ! $lang || ! $term_id ) { return $this->err( 'INVALID_PARAM', "Invalid entry in translations: lang=$lang term_id=$term_id", 'validation_failed' ); } $new_map[ $lang ] = $term_id; } if ( $dry_run ) { return $this->ok( [ 'dry_run' => true, 'would_link' => $new_map ] ); } // Merge with existing groups so we never drop languages not mentioned $merged = $new_map; if ( function_exists( 'pll_get_term_translations' ) ) { foreach ( $new_map as $term_id ) { $existing = pll_get_term_translations( $term_id ); foreach ( $existing as $existing_lang => $existing_id ) { if ( ! isset( $merged[ $existing_lang ] ) ) { $merged[ $existing_lang ] = $existing_id; } } } } if ( function_exists( 'pll_save_term_translations' ) ) { pll_save_term_translations( $merged ); } return $this->ok( [ 'linked' => true, 'group' => $merged ], [ 'target_type' => 'pll_term_translation', 'target_id' => implode( '-', array_values( $merged ) ) ] ); } }