Table of Contents
WPML хранит связи между переводами (постами, таксономиями, терминами и т.д.) в своих собственных таблицах, основная из которых — wp_icl_translations.
Таблица wp_icl_translations
Эта таблица — ключевая для хранения связей переводов.
Каждая строка соответствует одному объекту (посту, термину, меню и т.д.) в определённом языке.
| Поле | Назначение |
|---|---|
translation_id | Уникальный ID строки в этой таблице |
element_type | Тип элемента: например, post_post, post_page, tax_category, tax_post_tag и т.п. |
element_id | ID самого элемента из соответствующей таблицы (wp_posts.ID или wp_terms.term_id) |
trid | Translation 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_id | element_type | element_id | trid | language_code | source_language_code |
|---|---|---|---|---|---|
| 1 | post_post | 101 | 500 | en | NULL |
| 2 | post_post | 102 | 500 | de | en |
Одинаковый trid = 500 говорит WPML, что это переводы одного и того же поста.
Пример связи таксономий
Для таксономий аналогично, но element_type будет tax_ + название таксономии:
| translation_id | element_type | element_id | trid | language_code | source_language_code |
|---|---|---|---|---|---|
| 11 | tax_category | 31 | 200 | en | NULL |
| 12 | tax_category | 32 | 200 | de | en |
| 13 | tax_category | 33 | 200 | fr | en |
Как программно узнать связи
Пример для поста:
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"
}
- Получает данные из запроса (
taxonomy,title,slug,language). - Проверяет, что указаны обязательные параметры.
- Определяет, активен ли WPML (
defined('ICL_SITEPRESS_VERSION')). - Вызывает соответствующий класс:
TaxonomyImporterWpml— если WPML включён.TaxonomyImporter— если WPML не используется (создаёт обычные термины).
- Возвращает 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, и т.п.).
Основной цикл по термам
Для каждого переданного терма:
- Проверяется, есть ли уже запись с таким
tridи языком (get_term_id_by_trid_and_lang()). - Если есть → обновляется (включая Rank Math и ACF поля).
- Если нет → создаётся новый термин:
- через
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_id | element_type | element_id | trid | language_code | source_language_code |
|---|---|---|---|---|---|
| 7002 | tax_area | 124 | 500 | ru | en |
То есть:
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_id | element_type | element_id | trid | language_code | source_language_code |
|---|---|---|---|---|---|
| 7001 | tax_area | 123 | 501 | en | NULL |
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.
