From ff299d3897e3b464b9ad64e20b105900f9254275 Mon Sep 17 00:00:00 2001 From: colorlink Date: Thu, 15 Oct 2026 18:17:49 +0000 Subject: [PATCH] =?UTF-8?q?feat(batch4):=20WPML=20translation=20tools=20?= =?UTF-8?q?=E2=80=94=20languages,=20post=20language,=20translation=20map,?= =?UTF-8?q?=20missing-translation=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- includes/Extensions/Batch4.php | 127 +++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 includes/Extensions/Batch4.php diff --git a/includes/Extensions/Batch4.php b/includes/Extensions/Batch4.php new file mode 100644 index 0000000..d0d169c --- /dev/null +++ b/includes/Extensions/Batch4.php @@ -0,0 +1,127 @@ + 'object', 'properties' => $props]; + if ($required) $schema['required'] = $required; + return ['name' => $name, 'description' => $desc, 'scope' => $scope, + 'handler' => 'AskTamMCP\\Extensions\\Batch4::' . $method, 'inputSchema' => $schema]; + }; + + $registry->register($def('wpml_list_languages', 'List all WPML languages: code, name, native name, active flag, and which is the default.', [], [], 'read', 'listLanguages')); + + $registry->register($def('wpml_get_post_language', 'Get the WPML language code of a post.', + ['post_id' => ['type' => 'integer']], ['post_id'], 'read', 'postLanguage')); + + $registry->register($def('wpml_get_translations', 'Get the full translation group of a post: for each language, the translated post id, title, status, and whether it exists.', + ['post_id' => ['type' => 'integer']], ['post_id'], 'read', 'getTranslations')); + + $registry->register($def('wpml_missing_translations', 'List the WPML languages a post is NOT yet translated into.', + ['post_id' => ['type' => 'integer']], ['post_id'], 'read', 'missingTranslations')); + + $registry->register($def('wpml_audit', 'Audit published posts/pages for missing translations across all active WPML languages. Returns per-language missing counts and the offenders.', + ['post_types' => ['type' => 'array', 'items' => ['type' => 'string'], 'default' => ['page', 'post']], 'per_page' => ['type' => 'integer', 'default' => 50]], + [], 'read', 'audit')); + } + + private static function languages(): array { + return (array) apply_filters('wpml_active_languages', null, ['skip_missing' => 0]); + } + + private static function elementType(int $id): string { + return 'post_' . get_post_type($id); + } + + public static function listLanguages(array $args): array { + $langs = self::languages(); + $default = apply_filters('wpml_default_language', null); + $out = []; + foreach ($langs as $code => $l) { + $out[] = ['code' => $code, 'name' => $l['translated_name'] ?? ($l['native_name'] ?? $code), + 'native_name' => $l['native_name'] ?? '', 'active' => !empty($l['active']), + 'is_default' => $code === $default]; + } + return ['languages' => $out, 'default' => $default, 'count' => count($out)]; + } + + public static function postLanguage(array $args): array { + $id = (int) ($args['post_id'] ?? 0); + if (!$id) throw new \RuntimeException('post_id is required.'); + $details = apply_filters('wpml_post_language_details', null, $id); + $code = is_array($details) ? ($details['language_code'] ?? null) : null; + return ['post_id' => $id, 'language' => $code, 'details' => $details ?: null]; + } + + public static function getTranslations(array $args): array { + $id = (int) ($args['post_id'] ?? 0); + if (!$id) throw new \RuntimeException('post_id is required.'); + $type = self::elementType($id); + $trid = apply_filters('wpml_element_trid', null, $id, $type); + if (!$trid) return ['post_id' => $id, 'trid' => null, 'translations' => [], 'note' => 'No translation group (trid) found for this post.']; + $translations = apply_filters('wpml_get_element_translations', null, $trid, $type); + $map = []; + foreach ((array) $translations as $code => $t) { + $tid = (int) ($t->element_id ?? 0); + $map[$code] = [ + 'post_id' => $tid, + 'title' => $tid ? get_the_title($tid) : '', + 'status' => $tid ? get_post_status($tid) : 'missing', + 'is_source' => !empty($t->original), + 'exists' => (bool) $tid, + ]; + } + return ['post_id' => $id, 'trid' => $trid, 'translations' => $map, 'count' => count($map)]; + } + + public static function missingTranslations(array $args): array { + $id = (int) ($args['post_id'] ?? 0); + $group = self::getTranslations($args); + $have = array_keys($group['translations']); + $all = array_keys(self::languages()); + $missing = array_values(array_diff($all, $have)); + return ['post_id' => $id, 'missing_languages' => $missing, 'missing_count' => count($missing), 'translated_into' => $have]; + } + + public static function audit(array $args): array { + $types = $args['post_types'] ?? ['page', 'post']; + $per = (int) ($args['per_page'] ?? 50); + $default = apply_filters('wpml_default_language', null); + $allLangs = array_keys(self::languages()); + // Audit the default-language originals. + $posts = get_posts(['post_type' => $types, 'post_status' => 'publish', 'posts_per_page' => $per, 'fields' => 'ids', + 'suppress_filters' => false]); + $perLang = array_fill_keys($allLangs, 0); + $offenders = []; + foreach ($posts as $pid) { + $pid = (int) $pid; + $details = apply_filters('wpml_post_language_details', null, $pid); + $lang = is_array($details) ? ($details['language_code'] ?? '') : ''; + if ($default && $lang && $lang !== $default) continue; // only audit source-language posts + $g = self::getTranslations(['post_id' => $pid]); + $have = array_keys($g['translations']); + $missing = array_values(array_diff($allLangs, $have)); + if ($missing) { + foreach ($missing as $m) $perLang[$m]++; + $offenders[] = ['post_id' => $pid, 'title' => get_the_title($pid), 'missing' => $missing]; + } + } + return ['audited' => count($posts), 'languages' => $allLangs, 'missing_per_language' => $perLang, + 'offenders' => $offenders, 'offender_count' => count($offenders)]; + } +}