WPML: как WPML хранит связи между переводами
как WPML хранит связи между переводами
Glossary overview

WPML: как WPML хранит связи между переводами

WPML хранит связи между переводами (постами, таксономиями, терминами и т.д.) в своих собственных таблицах, основная из которых — wp_icl_translations.

Таблица wp_icl_translations

Эта таблица — ключевая для хранения связей переводов.
Каждая строка соответствует одному объекту (посту, термину, меню и т.д.) в определённом языке.

ПолеНазначение
translation_idУникальный ID строки в этой таблице
element_typeТип элемента: например, post_post, post_page, tax_category, tax_post_tag и т.п.
element_idID самого элемента из соответствующей таблицы (wp_posts.ID или wp_terms.term_id)
tridTranslation group ID — группа переводов (все языковые версии одного и того же объекта имеют одинаковый trid)
language_codeКод языка (en, de, fr, uk, и т.д.)
source_language_codeЕсли это перевод — здесь указывается исходный язык (например, en). Если это оригинал, поле NULL.

Вот так хранятся таксономии.

Таксономия Services.

Таксономия Metro.

Таксономия Options.

Таксономия Area.

Вот так вот WPML хранит связи пост типов.

Тип элемента: например, post_post, post_page, post_models.

Даже пункты меню (nav_menu_item) WPML тоже хранит в таблице wp_icl_translations

WPML также создаёт переводы для вложений (post_type = attachment), и они тоже хранятся в таблице wp_icl_translations с типом post_attachment.

Acf поля WPML также добавляет в wp_icl_translations, и тип там указан как post_acf-field.

Пример связи постов

Допустим, у тебя есть 2 поста:

  • ID 101 — английская версия (language_code = 'en')
  • ID 102 — немецкая версия (language_code = 'de')

Тогда в таблице wp_icl_translations будет:

translation_idelement_typeelement_idtridlanguage_codesource_language_code
1post_post101500enNULL
2post_post102500deen

Одинаковый trid = 500 говорит WPML, что это переводы одного и того же поста.

Пример связи таксономий

Для таксономий аналогично, но element_type будет tax_ + название таксономии:

translation_idelement_typeelement_idtridlanguage_codesource_language_code
11tax_category31200enNULL
12tax_category32200deen
13tax_category33200fren

Как программно узнать связи

Пример для поста:

global $wpdb, $sitepress;
$post_id = 101;

$trid = $sitepress->get_element_trid($post_id, 'post_post');
$translations = $sitepress->get_element_translations($trid, 'post_post');

foreach ($translations as $lang => $translation) {
    echo "$lang => " . $translation->element_id . "\n";
}

Пример для термов:

$trid = $sitepress->get_element_trid($term_id, 'tax_category');
$translations = $sitepress->get_element_translations($trid, 'tax_category');

Пример автоматического создания термов такосномии и установка для них переводов.

Точка входа – REST API point.

<?php

namespace Src\Routes;

use Src\Includes\TaxonomyImporter;
use Src\Includes\TaxonomyImporterWpml;
use Src\SPRMTool;
use Throwable;
use WP_REST_Request;
use WP_REST_Response;

class CreateTerms
{
    public function register(): void
    {
        register_rest_route('sprm/v1', '/create-terms', [
            'methods'             => 'POST',
            'callback'            => [$this, 'handle'],
            'permission_callback' => [SPRMTool::class, 'checkAuth'],
        ]);
    }

    public function handle(WP_REST_Request $request): WP_REST_Response
    {
        $params = $request->get_json_params() ?: $request->get_params();

        $taxonomy_slug = sanitize_key($params['taxonomy'] ?? '');
        $term_title    = sanitize_text_field($params['title'] ?? '');
        $slug          = sanitize_title($params['slug'] ?? '');
        $language      = sanitize_text_field($params['language'] ?? '');

        if (!$taxonomy_slug || !$term_title) {
            error_log('[CreateTerms] Missing required fields: taxonomy or title');
            return new WP_REST_Response(['error' => 'taxonomy and title are required'], 400);
        }

        $has_wpml = defined('ICL_SITEPRESS_VERSION');

        $term_data = [
            'title'    => $term_title,
            'slug'     => $slug,
            'language' => $language,
        ];

        try {
            if ($has_wpml && !empty($language)) {
                $importer = new TaxonomyImporterWpml();
                $result = $importer->import([$term_data], $taxonomy_slug, $language, false, false);
                error_log('[CreateTerms] WPML import result: ' . print_r($result, true));
            } else {
                $importer = new TaxonomyImporter();
                $result = $importer->import($term_data, $taxonomy_slug, $language, false, false);
            }

            if (!$result) {
                error_log('[CreateTerms] Import failed');
                return new WP_REST_Response(['error' => 'Import failed'], 500);
            }

            return new WP_REST_Response([
                'success'  => true,
                'taxonomy' => $taxonomy_slug,
                'term'     => $term_title,
                'terms'    => $result,
            ], 200);

        } catch (Throwable $e) {
            error_log('[CreateTerms] Exception: ' . $e->getMessage());
            return new WP_REST_Response([
                'error' => 'Exception: ' . $e->getMessage(),
            ], 500);
        }
    }
}

Логика CreateTerms::handle()

{
  "taxonomy": "area",
  "title": "Vilnius",
  "slug": "vilnius",
  "language": "en"
}
  1. Получает данные из запроса (taxonomy, title, slug, language).
  2. Проверяет, что указаны обязательные параметры.
  3. Определяет, активен ли WPML (defined('ICL_SITEPRESS_VERSION')).
  4. Вызывает соответствующий класс:
    • TaxonomyImporterWpml — если WPML включён.
    • TaxonomyImporter — если WPML не используется (создаёт обычные термины).
  5. Возвращает JSON-ответ с результатами.

Что делает TaxonomyImporterWpml

Это основной класс, выполняющий фактический импорт терминов с учётом языков WPML.
Его цель — создать или обновить термины во всех активных языках, сохранить связи (trid) и обновить метаданные (ACF и Rank Math).

<?php

namespace Src\Includes;

class TaxonomyImporterWpml
{
    public function import(
        array $terms_data,
        string $taxonomy_name,
        string $lang_code,
        bool $overwrite_description = true,
        bool $import_meta = true
    ): array|bool {
        global $wpdb, $sitepress;

        error_log("[TaxonomyImporterWpml] 🔹 import() start: taxonomy={$taxonomy_name}, lang={$lang_code}, terms=" . count($terms_data));

        remove_filter('pre_term_description', 'wp_filter_kses');
        remove_filter('term_description', 'wp_kses_data');

        if (!taxonomy_exists($taxonomy_name)) {
            register_taxonomy(
                $taxonomy_name,
                'models',
                [
                    'label'        => ucfirst($taxonomy_name),
                    'hierarchical' => true,
                    'show_ui'      => true,
                    'show_in_rest' => true,
                    'rewrite'      => ['slug' => $taxonomy_name],
                ]
            );
            error_log("[TaxonomyImporterWpml] ✅ Registered new taxonomy '{$taxonomy_name}'");
        }

        $this->taxonomy_is_translatable($taxonomy_name);

        $languages = apply_filters('wpml_active_languages', null, ['skip_missing' => 0]);
        if (empty($languages)) {
            error_log("[TaxonomyImporterWpml] ❌ No WPML languages found!");
            return false;
        }

        $created_terms = [];

        foreach ($terms_data as $term) {
            $term_name  = trim($term['title'] ?? '');
            $trid       = intval($term['trid'] ?? 0);
            $language   = $term['language'] ?? $lang_code;

            if (!$term_name) {
                error_log("[TaxonomyImporterWpml] ⚠️ Empty term title, skipping.");
                continue;
            }

            $existing_id = null;
            if ($trid && $sitepress) {
                $existing_id = $this->get_term_id_by_trid_and_lang($trid, $language, $taxonomy_name);
            }

            if ($existing_id) {
                error_log("[TaxonomyImporterWpml] 🔁 Updating existing term '{$term_name}' (ID={$existing_id}, lang={$language}, trid={$trid})");

                $description = $overwrite_description ? wp_kses_post($term['description'] ?? '') : '';

                wp_update_term($existing_id, $taxonomy_name, [
                    'name' => $term_name,
                    'slug' => sanitize_title($term_name),
                ]);

                if ($description !== '') {
                    global $wpdb;
                    $wpdb->update(
                        $wpdb->term_taxonomy,
                        ['description' => $description],
                        ['term_id' => $existing_id, 'taxonomy' => $taxonomy_name]
                    );
                }

                if ($import_meta) {
                    update_term_meta($existing_id, 'rank_math_title', $term['meta_title'] ?? '');
                    update_term_meta($existing_id, 'rank_math_description', $term['meta_description'] ?? '');

                    update_field('term_title', $term['displayed_title'] ?? '', 'term_' . $existing_id);
                    update_field('term_description', $term['description'] ?? '', 'term_' . $existing_id);
                }

                $created_terms[] = [
                    'id'       => $existing_id,
                    'title'    => $term_name,
                    'language' => $language,
                    'trid'     => $trid,
                    'updated'  => true,
                ];

                continue;
            }


            error_log("[TaxonomyImporterWpml] ➕ Creating new term '{$term_name}' (lang={$language})");

            $inserted = wp_insert_term($term_name, $taxonomy_name, [
                'description' => wp_kses_post($term['description'] ?? ''),
                'slug'        => sanitize_title($term_name),
            ]);

            if (is_wp_error($inserted)) {
                if ($inserted->get_error_code() === 'term_exists') {
                    $existing_id = (int) $inserted->get_error_data();
                    error_log("[TaxonomyImporterWpml] ⚠️ Term already exists: {$term_name} (ID={$existing_id})");
                } else {
                    error_log("[TaxonomyImporterWpml] ❌ Error creating '{$term_name}': " . $inserted->get_error_message());
                    continue;
                }
            } else {
                $existing_id = $inserted['term_id'];
            }

            if ($sitepress) {
                if ($trid) {
                    $sitepress->set_element_language_details(
                        $existing_id,
                        'tax_' . $taxonomy_name,
                        $trid,
                        $language
                    );
                } else {
                    $sitepress->set_element_language_details(
                        $existing_id,
                        'tax_' . $taxonomy_name,
                        null,
                        $language,
                        null
                    );
                    $trid = $sitepress->get_element_trid($existing_id, 'tax_' . $taxonomy_name);
                    error_log("[TaxonomyImporterWpml] 🧩 New TRID created: {$trid}");
                }

                $created_terms[] = [
                    'id'        => $existing_id,
                    'title'     => $term_name,
                    'language'  => $language,
                    'trid'      => $trid,
                    'updated'   => false,
                ];


                foreach ($languages as $lang_code_iter => $lang_info) {
                    if ($lang_code_iter === $language) continue;

                    $translated_title = $term_name . ' (' . strtoupper($lang_code_iter) . ')';
                    $translated_slug  = sanitize_title($translated_title);

                    $translated = wp_insert_term($translated_title, $taxonomy_name, [
                        'description' => wp_kses_post($term['description'] ?? ''),
                        'slug'        => $translated_slug,
                    ]);

                    if (is_wp_error($translated)) {
                        if ($translated->get_error_code() === 'term_exists') {
                            $translated_id = (int)$translated->get_error_data();
                            error_log("[TaxonomyImporterWpml] ⚠️ Translation already exists for {$translated_title} (ID={$translated_id})");
                        } else {
                            error_log("[TaxonomyImporterWpml] ❌ Error creating translation {$translated_title}: " . $translated->get_error_message());
                            continue;
                        }
                    } else {
                        $translated_id = $translated['term_id'];
                    }

                    $sitepress->set_element_language_details(
                        $translated_id,
                        'tax_' . $taxonomy_name,
                        $trid,
                        $lang_code_iter,
                        $language
                    );

                    if ($import_meta) {
                        update_term_meta($translated_id, 'rank_math_title', $term['meta_title'] ?? '');
                        update_term_meta($translated_id, 'rank_math_description', $term['meta_description'] ?? '');
                        update_field('term_title', $translated_title, 'term_' . $translated_id);
                        update_field('term_description', $term['description'] ?? '', 'term_' . $translated_id);
                    }

                    $created_terms[] = [
                        'id'       => $translated_id,
                        'title'    => $translated_title,
                        'language' => $lang_code_iter,
                        'trid'     => $trid,
                        'updated'  => false,
                    ];

                    error_log("[TaxonomyImporterWpml] 🌍 Created translation for '{$term_name}' in {$lang_code_iter}");
                }
            }

        }

        error_log("[TaxonomyImporterWpml] ✅ Finished. Processed terms: " . count($created_terms));

        add_filter('pre_term_description', 'wp_filter_kses');
        add_filter('term_description', 'wp_kses_data');

        return $created_terms;
    }

    /**
     * Ищет ID терма по TRID и языку
     */
    private function get_term_id_by_trid_and_lang(int $trid, string $lang, string $taxonomy_name): ?int
    {
        global $sitepress, $wpdb;
        if (!$trid || !$lang) {
            return null;
        }

        $element_type = 'tax_' . $taxonomy_name;
        $table = $wpdb->prefix . 'icl_translations';

        $term_id = $wpdb->get_var($wpdb->prepare(
            "SELECT element_id FROM {$table}
             WHERE trid = %d AND language_code = %s AND element_type = %s",
            $trid,
            $lang,
            $element_type
        ));

        return $term_id ? (int)$term_id : null;
    }

    private function taxonomy_is_translatable(string $taxonomy_name): void
    {
        if (!defined('ICL_SITEPRESS_VERSION')) {
            return;
        }

        global $sitepress;

        if (!isset($sitepress)) {
            return;
        }

        $translatable_taxonomies = (array)$sitepress->get_translatable_taxonomies();

        if (!in_array($taxonomy_name, $translatable_taxonomies, true)) {
            if (function_exists('do_action')) {
                do_action('wpml_register_taxonomy_for_translation', $taxonomy_name);
            }

            $settings = get_option('icl_sitepress_settings');
            if (!isset($settings['taxonomies_sync_option'][$taxonomy_name])) {
                $settings['taxonomies_sync_option'][$taxonomy_name] = 1;
                update_option('icl_sitepress_settings', $settings);
            }
        }
    }
}

Ключевые этапы работы

1. Проверка и регистрация таксономии

Если таксономия ещё не зарегистрирована — создаётся “на лету” через register_taxonomy().

2. Проверка, что таксономия переводимая

Метод taxonomy_is_translatable() включает поддержку перевода для указанной таксономии в WPML (через wpml_register_taxonomy_for_translation).

3. Получение списка активных языков

$languages = apply_filters('wpml_active_languages', null, ['skip_missing' => 0]);

Позволяет узнать, какие языки включены (en, ru, de, и т.п.).

Основной цикл по термам

Для каждого переданного терма:

  1. Проверяется, есть ли уже запись с таким trid и языком (get_term_id_by_trid_and_lang()).
  2. Если есть → обновляется (включая Rank Math и ACF поля).
  3. Если нет → создаётся новый термин:
    • через wp_insert_term(),
    • связывается с WPML (set_element_language_details),
    • получает trid.

Автоматическое создание переводов

После создания оригинального терма:

foreach ($languages as $lang_code_iter => $lang_info) {
    if ($lang_code_iter === $language) continue;
    // создаёт перевод для каждого другого языка
}
  • Для каждого другого языка создаётся “заглушка” перевода.
  • К имени добавляется суффикс, например Vilnius (DE).
  • Все переводы получают один и тот же trid, чтобы WPML понимал, что это одна группа переводов.
  • Метаданные (Rank Math, ACF) копируются.

Создались по 5 термов. Они сразу связаны между собой, как переводы.

В базе данных появились 15 новых записей в таблице wp_icl_translations.

Мы сами создаем trid для записей?

В нашем коде TaxonomyImporterWpml мы не создаём TRID вручную — мы получаем или генерируем его автоматически через WPML API.

Как это происходит в коде

1. Когда в $term_data уже передан trid

$trid = intval($term['trid'] ?? 0);
...
if ($trid && $sitepress) {
    $existing_id = $this->get_term_id_by_trid_and_lang($trid, $language, $taxonomy_name);
}

Если в массиве $term указан trid, код будет искать существующий термин с этим trid и языком.
→ В этом случае trid уже должен существовать (например, если импорт идёт из другой системы).

2. Если trid не передан

if ($trid) {
    $sitepress->set_element_language_details(
        $existing_id,
        'tax_' . $taxonomy_name,
        $trid,
        $language
    );
} else {
    $sitepress->set_element_language_details(
        $existing_id,
        'tax_' . $taxonomy_name,
        null,
        $language,
        null
    );
    $trid = $sitepress->get_element_trid($existing_id, 'tax_' . $taxonomy_name);
    error_log("[TaxonomyImporterWpml] 🧩 New TRID created: {$trid}");
}

Если $trid не передан (null), вызывается, в этом случае WPML сам создаёт новый TRID в таблице wp_icl_translations.

$sitepress->set_element_language_details(..., null, $language, null);

После этого мы получаем новый trid.

$sitepress->get_element_trid($existing_id, 'tax_' . $taxonomy_name);

Создание переводов

Дальше тот же trid передаётся при создании переводов:

$sitepress->set_element_language_details(
    $translated_id,
    'tax_' . $taxonomy_name,
    $trid,          // ← используем тот же trid
    $lang_code_iter,
    $language
);

Это связывает все термы (EN, RU, DE и т.д.) в одну группу переводов.

Всю магию делает функция set_element_language_details.

Это ключевая функция WPML, и именно она отвечает за то, как объект (пост, термин, вложение и т.д.) “привязывается” к языку и группе переводов (trid).

Что она принимает:

$sitepress->set_element_language_details(
    int $element_id,
    string $element_type,
    ?int $trid,
    string $language_code,
    ?string $source_language_code = null
);

То есть:

  • element_id = 124 — ID записи (терма, поста и тд.);
  • element_type = например'tax_area';
  • trid = 500 — принадлежит к группе переводов;
  • language_code = 'ru' — язык записи;
  • source_language_code = 'en' — перевод с английского.

Если trid передан

$sitepress->set_element_language_details($term_id, 'tax_area', 500, 'ru', 'en');

Если trid передан, тоset_element_language_details() обновит существующую запись в таблице wp_icl_translations, если она уже существует.
А если записи ещё нет — она создаст новую строку с указанным trid

В БД:

translation_idelement_typeelement_idtridlanguage_codesource_language_code
7002tax_area124500ruen

То есть:

  • element_id = 124 — ID терма;
  • element_type = 'tax_area';
  • trid = 500 — принадлежит к группе переводов;
  • language_code = 'ru' — язык записи;
  • source_language_code = 'en' — перевод с английского.

Если trid = null

$sitepress->set_element_language_details($term_id, 'tax_area', null, 'en', null);

WPML создаёт новый TRID — новую “группу переводов”.

📦 В БД появится новая запись:

translation_idelement_typeelement_idtridlanguage_codesource_language_code
7001tax_area123501enNULL

trid = 501 назначается автоматически и уникален для всей группы.

Примеры

Создать новый оригинал:

$sitepress->set_element_language_details(123, 'tax_area', null, 'en');
  • WPML создаст trid = 9001
  • и пометит элемент как оригинал английского терма.

Добавить перевод:

$sitepress->set_element_language_details(124, 'tax_area', 9001, 'de', 'en');
  • WPML свяжет term_id=124 как немецкий перевод английского term_id=123.

Получить TRID позже:

$trid = $sitepress->get_element_trid(123, 'tax_area');

Функция get_element_trid вернёт 9001.

Итого

set_element_language_details() — это функция, которая “регистрирует” связь между объектом WordPress и WPML,
а именно:

  • задаёт, к какой группе переводов (trid) он относится;
  • устанавливает язык элемента;
  • указывает, от какого языка он переведён;
  • при необходимости создаёт новый TRID.