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.strings.list', [ 'class' => 'read', 'required_scopes' => [ 'read.wp', $scopes ], 'dependency' => $dep, 'description' => 'List all registered Polylang strings with their current translations per language.', 'input_schema' => [ 'group' => [ 'type' => 'string', 'description' => 'Filter by group name (e.g. "WordPress", "polylang"). Omit for all groups.' ], ], 'handler' => [ $this, 'handle_strings_list' ], ] ); $this->register( 'pll.string.get', [ 'class' => 'read', 'required_scopes' => [ 'read.wp', $scopes ], 'dependency' => $dep, 'description' => 'Get a registered Polylang string and all its translations.', 'input_schema' => [ 'name' => [ 'type' => 'string', 'required' => true, 'description' => 'Registered string name (e.g. "Site Tagline").' ], 'group' => [ 'type' => 'string', 'required' => true, 'description' => 'String group (e.g. "WordPress").' ], ], 'handler' => [ $this, 'handle_string_get' ], ] ); $this->register( 'pll.string.set', [ 'class' => 'write', 'required_scopes' => [ 'write.wp.posts', $scopes ], 'supports_dry_run' => true, 'dependency' => $dep, 'description' => 'Set the translation of a registered Polylang string for a specific language.', 'input_schema' => [ 'name' => [ 'type' => 'string', 'required' => true, 'description' => 'Registered string name (e.g. "Site Tagline").' ], 'group' => [ 'type' => 'string', 'required' => true, 'description' => 'String group (e.g. "WordPress").' ], 'language' => [ 'type' => 'string', 'required' => true, 'description' => 'Language slug (e.g. "ro", "en", "uk").' ], 'translation' => [ 'type' => 'string', 'required' => true, 'description' => 'Translated string value.' ], ], 'handler' => [ $this, 'handle_string_set' ], ] ); $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_strings_list( array $args, array $key_record, bool $dry_run ): array { if ( ! function_exists( 'pll_get_strings' ) || ! function_exists( 'pll_languages_list' ) ) { return $this->err( 'UNAVAILABLE', 'pll_get_strings not available in this Polylang version', 'error', 500 ); } $filter_group = sanitize_text_field( $args['group'] ?? '' ); $all_strings = pll_get_strings(); $lang_slugs = pll_languages_list( [ 'fields' => 'slug' ] ); $result = []; foreach ( $all_strings as $s ) { if ( $filter_group && $s['group'] !== $filter_group ) { continue; } $translations = []; foreach ( $lang_slugs as $lang ) { $translations[ $lang ] = pll_translate_string( $s['string'], $lang ); } $result[] = [ 'name' => $s['name'], 'string' => $s['string'], 'group' => $s['group'], 'multiline' => ! empty( $s['multiline'] ), 'translations' => $translations, ]; } return $this->ok( [ 'strings' => $result, 'total' => count( $result ) ] ); } public function handle_string_get( array $args, array $key_record, bool $dry_run ): array { $name = sanitize_text_field( $args['name'] ?? '' ); $group = sanitize_text_field( $args['group'] ?? '' ); if ( ! $name || ! $group ) { return $this->err( 'MISSING_PARAM', 'Parameters name and group are required', 'validation_failed' ); } if ( ! function_exists( 'pll_get_strings' ) ) { return $this->err( 'UNAVAILABLE', 'pll_get_strings not available in this Polylang version', 'error', 500 ); } $found = null; foreach ( pll_get_strings() as $s ) { if ( $s['name'] === $name && $s['group'] === $group ) { $found = $s; break; } } if ( ! $found ) { return $this->err( 'NOT_FOUND', "String '$name' in group '$group' not found", 'error', 404 ); } $lang_slugs = pll_languages_list( [ 'fields' => 'slug' ] ); $translations = []; foreach ( $lang_slugs as $lang ) { $translations[ $lang ] = pll_translate_string( $found['string'], $lang ); } return $this->ok( [ 'name' => $found['name'], 'string' => $found['string'], 'group' => $found['group'], 'multiline' => ! empty( $found['multiline'] ), 'translations' => $translations, ] ); } public function handle_string_set( array $args, array $key_record, bool $dry_run ): array { $name = sanitize_text_field( $args['name'] ?? '' ); $group = sanitize_text_field( $args['group'] ?? '' ); $language = sanitize_key( $args['language'] ?? '' ); $translation = $args['translation'] ?? ''; if ( ! $name || ! $group || ! $language ) { return $this->err( 'MISSING_PARAM', 'Parameters name, group and language are required', 'validation_failed' ); } if ( ! AICOM_Policy_Engine::check_language_allowlist( $key_record, $language ) ) { return $this->err( 'DENIED_ALLOWLIST', "Language not in allowlist: $language", 'denied_allowlist', 403 ); } if ( ! function_exists( 'pll_get_strings' ) ) { return $this->err( 'UNAVAILABLE', 'pll_get_strings not available in this Polylang version', 'error', 500 ); } // Find the original string value $found = null; foreach ( pll_get_strings() as $s ) { if ( $s['name'] === $name && $s['group'] === $group ) { $found = $s; break; } } if ( ! $found ) { return $this->err( 'NOT_FOUND', "String '$name' in group '$group' not found. Use pll.strings.list to see available strings.", 'error', 404 ); } if ( ! isset( PLL()->model ) || ! method_exists( PLL()->model, 'get_language' ) ) { return $this->err( 'UNAVAILABLE', 'Polylang model not initialised', 'error', 500 ); } $lang_obj = PLL()->model->get_language( $language ); if ( ! $lang_obj ) { return $this->err( 'NOT_FOUND', "Language not found: $language", 'error', 404 ); } if ( $dry_run ) { return $this->ok( [ 'dry_run' => true, 'name' => $name, 'group' => $group, 'language' => $language, 'translation' => $translation, ] ); } if ( ! class_exists( 'PLL_MO' ) ) { return $this->err( 'UNAVAILABLE', 'PLL_MO class not available', 'error', 500 ); } $mo = new PLL_MO(); $mo->import_from_db( $lang_obj ); $mo->add_entry( $mo->make_entry( $found['string'], $translation ) ); $mo->export_to_db( $lang_obj ); return $this->ok( [ 'name' => $name, 'group' => $group, 'language' => $language, 'translation' => $translation, 'updated' => true, ], [ 'target_type' => 'pll_string', 'target_id' => "$group/$name/$language" ] ); } 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 ) ) ] ); } }