Script PHP per vincular traduccions de posts/CPT importats a WordPress amb WPML.
Què fa
Vincula automàticament posts o custom post types que han estat importats amb camps meta de traducció, connectant-los correctament a WPML perquè apareguin les banderes i el selector d’idiomes funcioni.
Requisits previs
IMPORTANT: Abans d’importar els posts, assegura’t d’incloure aquests dos custom fields:
wpmltranslationid– ID que agrupa les traduccions del mateix contingut- Exemple: Post CA id=100, Post ES id=200, Post EN id=300 → tots amb
wpmltranslationid = "1"
- Exemple: Post CA id=100, Post ES id=200, Post EN id=300 → tots amb
wpmllanguagecode– Codi d’idioma de cada post- Valors:
ca,es,en(o els idiomes que tinguis configurats)
- Valors:
Exemple d’importació CSV:
id,title,wpmltranslationid,wpmllanguagecode
100,"Títol en català",1,ca
200,"Título en español",1,es
300,"Title in english",1,en
Configuració
Edita les variables al principi del fitxer:
$CONFIG = [
'security_key' => 'mmkt2024', // Canvia-ho per seguretat
'post_type' => 'post', // 'post', 'page', 'product', etc.
'translation_id_meta' => 'wpmltranslationid',
'language_code_meta' => 'wpmllanguagecode',
'languages' => ['ca', 'es', 'en'], // Idiomes del teu site
'original_lang' => 'ca', // Idioma original/principal
'require_complete_groups' => true, // true = només vincular grups amb tots els idiomes
'allow_partial_groups' => false, // true = vincular també grups sense idioma original
];
Ús
- Puja el fitxer
wpml-link-generic.phpa l’arrel del WordPress - Accedeix via navegador:
https://elseuteu.com/wpml-link-generic.php?key=mmkt2024
- Segueix els passos:
- Verificació: Veure estadístiques i exemples
- Previsualització: Revisar què es vincularà
- Execució: Fer la vinculació
- Elimina el fitxer després d’usar-lo
Advertències
- ⚠️ Fes backup de la base de dades abans d’executar
- ⚠️ Revisa la previsualització abans d’executar
- ⚠️ Els grups sense l’idioma original (CA per defecte) no es processaran
- ⚠️ Si un grup ja està vinculat correctament, s’ignora
- ⚠️ Aquest script només funciona si els posts ja tenen els custom fields
wpmltranslationidiwpmllanguagecode
Casos d’ús
Importació estàndard (3 idiomes complets)
'require_complete_groups' => true // Només vincular CA+ES+EN
Importació parcial (acceptar grups incomplets)
'require_complete_groups' => false // Vincular fins i tot si falta algun idioma
'allow_partial_groups' => true // Vincular grups sense idioma original
Altres post types
'post_type' => 'product' // Per WooCommerce
'post_type' => 'page' // Per pàgines
Noms de camps personalitzats
'translation_id_meta' => 'custom_trans_id',
'language_code_meta' => 'custom_lang_code',
Troubleshooting
“No s’han trobat posts” → Comprova que els meta fields wpmltranslationid i wpmllanguagecode existeixen als posts
“Grups sense CA” → Assegura’t que cada grup té un post amb wpmllanguagecode = ca
“Ja vinculat” → Els posts ja estan correctament vinculats a WPML, no cal fer res
<?php
/**
* Script genèric de vinculació WPML
* Author: Milimetric Marketing
* Plugin URI: https://milimetricmkt.com
*
* CONFIGURACIÓ (canvia aquests valors segons la teva importació):
*/
// === CONFIGURACIÓ EDITABLE ===
$CONFIG = [
// Clau de seguretat (canvia-la!)
'security_key' => 'mmkt2024',
// Post type a vincular (post, page, product, etc.)
'post_type' => 'post',
// Meta keys dels camps importats
'translation_id_meta' => 'wpmltranslationid', // Camp que agrupa les traduccions
'language_code_meta' => 'wpmllanguagecode', // Camp amb l'idioma (ca, es, en...)
// Configuració d'idiomes
'languages' => ['ca', 'es', 'en'], // Idiomes disponibles
'original_lang' => 'ca', // Idioma original
// Opcions
'require_complete_groups' => true, // true = només vincular grups amb tots els idiomes
'allow_partial_groups' => false, // true = vincular també grups sense idioma original
];
// === FI CONFIGURACIÓ ===
// Seguretat
if (!isset($_GET['key']) || $_GET['key'] !== $CONFIG['security_key']) {
die('Accés denegat');
}
require_once('wp-load.php');
define('WP_USE_THEMES', false);
$mode = $_GET['mode'] ?? 'check'; // check | link | execute
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Vinculació WPML - <?php echo strtoupper($CONFIG['post_type']); ?></title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; background: #f5f5f5; }
.container { background: white; padding: 30px; border-radius: 8px; max-width: 1400px; margin: 0 auto; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
h2, h3 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
.nav { margin: 20px 0; padding: 15px; background: #ecf0f1; border-radius: 5px; }
.nav a { display: inline-block; padding: 10px 20px; margin: 0 5px; background: #3498db; color: white; text-decoration: none; border-radius: 5px; }
.nav a:hover { background: #2980b9; }
.nav a.active { background: #2c3e50; }
.box { padding: 20px; margin: 20px 0; border-radius: 5px; border-left: 5px solid; }
.success { background: #d4edda; border-color: #28a745; color: #155724; }
.warning { background: #fff3cd; border-color: #ffc107; color: #856404; }
.error { background: #f8d7da; border-color: #dc3545; color: #721c24; }
.info { background: #d1ecf1; border-color: #17a2b8; color: #0c5460; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13px; }
th, td { padding: 12px 8px; text-align: left; border: 1px solid #ddd; }
th { background: #34495e; color: white; font-weight: 600; position: sticky; top: 0; }
tr:nth-child(even) { background: #f8f9fa; }
tr:hover { background: #e9ecef; }
.group { margin: 15px 0; padding: 15px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; }
.badge { display: inline-block; padding: 4px 8px; border-radius: 3px; font-size: 11px; font-weight: bold; margin: 0 3px; }
.badge-success { background: #28a745; color: white; }
.badge-warning { background: #ffc107; color: #000; }
.badge-danger { background: #dc3545; color: white; }
.badge-info { background: #17a2b8; color: white; }
.btn { display: inline-block; padding: 12px 24px; margin: 10px 5px; border-radius: 5px; text-decoration: none; font-weight: 600; cursor: pointer; border: none; }
.btn-primary { background: #3498db; color: white; }
.btn-primary:hover { background: #2980b9; }
.btn-success { background: #28a745; color: white; }
.btn-success:hover { background: #218838; }
.btn-danger { background: #dc3545; color: white; }
.btn-danger:hover { background: #c82333; }
.config-box { background: #e8f4f8; padding: 15px; margin: 20px 0; border-radius: 5px; font-family: monospace; font-size: 12px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; }
.stat-card { background: white; padding: 20px; border-radius: 5px; border: 2px solid #e0e0e0; text-align: center; }
.stat-value { font-size: 32px; font-weight: bold; color: #2c3e50; }
.stat-label { color: #7f8c8d; margin-top: 5px; }
</style>
</head>
<body>
<div class="container">
<h2>🔗 Vinculació WPML - <?php echo strtoupper($CONFIG['post_type']); ?></h2>
<div class="nav">
<a href="?key=<?php echo $CONFIG['security_key']; ?>&mode=check" class="<?php echo $mode == 'check' ? 'active' : ''; ?>">📋 Verificació</a>
<a href="?key=<?php echo $CONFIG['security_key']; ?>&mode=link" class="<?php echo $mode == 'link' ? 'active' : ''; ?>">🔗 Previsualització</a>
</div>
<?php
global $wpdb;
// Mostrar configuració actual
echo '<div class="config-box">';
echo '<strong>⚙️ Configuració actual:</strong><br>';
echo "Post Type: <strong>{$CONFIG['post_type']}</strong><br>";
echo "Meta Translation ID: <strong>{$CONFIG['translation_id_meta']}</strong><br>";
echo "Meta Language Code: <strong>{$CONFIG['language_code_meta']}</strong><br>";
echo "Idiomes: <strong>" . implode(', ', $CONFIG['languages']) . "</strong><br>";
echo "Idioma original: <strong>{$CONFIG['original_lang']}</strong><br>";
echo "Només grups complets: <strong>" . ($CONFIG['require_complete_groups'] ? 'Sí' : 'No') . "</strong><br>";
echo '</div>';
// Obtenir posts
$posts_data = $wpdb->get_results($wpdb->prepare("
SELECT
p.ID,
p.post_title,
p.post_type,
pm.meta_value as translation_id,
pml.meta_value as language_code,
t.trid as current_trid,
t.language_code as wpml_lang,
t.source_language_code
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = %s
LEFT JOIN {$wpdb->postmeta} pml ON p.ID = pml.post_id AND pml.meta_key = %s
INNER JOIN {$wpdb->prefix}icl_translations t
ON p.ID = t.element_id
AND t.element_type = %s
WHERE p.post_status = 'publish'
AND p.post_type = %s
ORDER BY pm.meta_value, pml.meta_value
",
$CONFIG['translation_id_meta'],
$CONFIG['language_code_meta'],
'post_' . $CONFIG['post_type'],
$CONFIG['post_type']
));
if (empty($posts_data)) {
echo '<div class="box error"><strong>❌ No s\'han trobat posts amb els meta fields especificats.</strong><br>';
echo 'Comprova que els camps <code>' . $CONFIG['translation_id_meta'] . '</code> i <code>' . $CONFIG['language_code_meta'] . '</code> existeixen.</div>';
echo '</div></body></html>';
exit;
}
// Agrupar per translation_id
$groups = [];
foreach ($posts_data as $post) {
$groups[$post->translation_id][] = $post;
}
// Analitzar grups
$stats = [
'total_posts' => count($posts_data),
'total_groups' => count($groups),
'complete_groups' => 0,
'incomplete_groups' => 0,
'with_original' => 0,
'without_original' => 0,
'needs_linking' => 0,
'already_linked' => 0
];
$processable_groups = [];
$examples_complete = [];
$examples_incomplete = [];
foreach ($groups as $tid => $posts) {
$langs = array_map(fn($p) => strtolower($p->language_code ?? ''), $posts);
$has_original = in_array($CONFIG['original_lang'], $langs);
$is_complete = count($posts) == count($CONFIG['languages']) &&
count(array_intersect($langs, $CONFIG['languages'])) == count($CONFIG['languages']);
if ($has_original) $stats['with_original']++;
else $stats['without_original']++;
if ($is_complete) {
$stats['complete_groups']++;
if (count($examples_complete) < 3) {
$examples_complete[] = ['tid' => $tid, 'posts' => $posts];
}
} else {
$stats['incomplete_groups']++;
if (count($examples_incomplete) < 3) {
$examples_incomplete[] = ['tid' => $tid, 'posts' => $posts];
}
}
// Comprovar si necessita vinculació
$trids = array_unique(array_map(fn($p) => $p->current_trid, $posts));
$needs_linking = count($trids) > 1;
if ($needs_linking) {
$stats['needs_linking']++;
} else {
$stats['already_linked']++;
}
// Determinar si es pot processar
$can_process = false;
if ($CONFIG['require_complete_groups']) {
$can_process = $is_complete && $has_original && $needs_linking;
} else {
$can_process = ($has_original || $CONFIG['allow_partial_groups']) && count($posts) >= 2 && $needs_linking;
}
if ($can_process) {
$processable_groups[$tid] = $posts;
}
}
// === MODE VERIFICACIÓ ===
if ($mode == 'check') {
echo '<h3>📊 Estadístiques</h3>';
echo '<div class="stats-grid">';
echo '<div class="stat-card"><div class="stat-value">' . $stats['total_posts'] . '</div><div class="stat-label">Posts totals</div></div>';
echo '<div class="stat-card"><div class="stat-value">' . $stats['total_groups'] . '</div><div class="stat-label">Grups de traducció</div></div>';
echo '<div class="stat-card"><div class="stat-value" style="color:#28a745">' . $stats['complete_groups'] . '</div><div class="stat-label">Grups complets</div></div>';
echo '<div class="stat-card"><div class="stat-value" style="color:#ffc107">' . $stats['incomplete_groups'] . '</div><div class="stat-label">Grups incomplets</div></div>';
echo '<div class="stat-card"><div class="stat-value" style="color:#17a2b8">' . $stats['with_original'] . '</div><div class="stat-label">Amb ' . strtoupper($CONFIG['original_lang']) . '</div></div>';
echo '<div class="stat-card"><div class="stat-value" style="color:#dc3545">' . count($processable_groups) . '</div><div class="stat-label">Per vincular</div></div>';
echo '</div>';
// Exemples de grups complets
if (!empty($examples_complete)) {
echo '<h3>✅ Exemples de grups COMPLETS</h3>';
foreach ($examples_complete as $ex) {
$trids = array_unique(array_map(fn($p) => $p->current_trid, $ex['posts']));
$needs = count($trids) > 1;
echo '<div class="group">';
echo '<strong>Translation ID: ' . $ex['tid'] . '</strong> ';
echo $needs ? '<span class="badge badge-warning">Necessita vinculació</span>' : '<span class="badge badge-success">Ja vinculat</span>';
echo '<table><tr><th>ID</th><th>Idioma</th><th>TRID</th><th>Source</th><th>Títol</th></tr>';
usort($ex['posts'], function($a, $b) use ($CONFIG) {
$order = array_flip($CONFIG['languages']);
return ($order[strtolower($a->language_code)] ?? 99) - ($order[strtolower($b->language_code)] ?? 99);
});
foreach ($ex['posts'] as $p) {
echo '<tr>';
echo '<td>' . $p->ID . '</td>';
echo '<td><strong>' . strtoupper($p->language_code) . '</strong></td>';
echo '<td>' . $p->current_trid . '</td>';
echo '<td>' . ($p->source_language_code ?: 'NULL') . '</td>';
echo '<td>' . substr($p->post_title, 0, 50) . '</td>';
echo '</tr>';
}
echo '</table></div>';
}
}
// Exemples de grups incomplets
if (!empty($examples_incomplete)) {
echo '<h3>⚠️ Exemples de grups INCOMPLETS</h3>';
foreach ($examples_incomplete as $ex) {
$langs = array_map(fn($p) => strtolower($p->language_code), $ex['posts']);
$missing = array_diff($CONFIG['languages'], $langs);
echo '<div class="group">';
echo '<strong>Translation ID: ' . $ex['tid'] . '</strong> ';
echo '<span class="badge badge-warning">Falten: ' . implode(', ', $missing) . '</span>';
echo '<table><tr><th>ID</th><th>Idioma</th><th>TRID</th><th>Títol</th></tr>';
foreach ($ex['posts'] as $p) {
echo '<tr>';
echo '<td>' . $p->ID . '</td>';
echo '<td><strong>' . strtoupper($p->language_code) . '</strong></td>';
echo '<td>' . $p->current_trid . '</td>';
echo '<td>' . substr($p->post_title, 0, 50) . '</td>';
echo '</tr>';
}
echo '</table></div>';
}
}
// Conclusió
echo '<div class="box info">';
echo '<h3>📝 Resum</h3>';
if (count($processable_groups) > 0) {
echo "<p><strong style='color:#28a745'>✅ Hi ha " . count($processable_groups) . " grups preparats per vincular.</strong></p>";
echo '<a href="?key=' . $CONFIG['security_key'] . '&mode=link" class="btn btn-primary">➡️ Anar a previsualització</a>';
} else {
if ($stats['needs_linking'] == 0) {
echo '<p><strong style="color:#28a745">✅ Tots els grups ja estan correctament vinculats!</strong></p>';
} else {
echo '<p><strong style="color:#dc3545">⚠️ No hi ha grups que compleixin els requisits per vincular.</strong></p>';
if ($CONFIG['require_complete_groups']) {
echo '<p>Intenta desactivar <code>require_complete_groups</code> si vols vincular grups incomplets.</p>';
}
}
}
echo '</div>';
}
// === MODE PREVISUALITZACIÓ ===
elseif ($mode == 'link') {
if (empty($processable_groups)) {
echo '<div class="box error"><strong>❌ No hi ha grups per vincular.</strong></div>';
echo '<a href="?key=' . $CONFIG['security_key'] . '&mode=check" class="btn btn-primary">◀️ Tornar a verificació</a>';
echo '</div></body></html>';
exit;
}
echo '<div class="box warning">';
echo '<h3>⚠️ MODE PREVISUALITZACIÓ</h3>';
echo '<p>Es vincularan <strong>' . count($processable_groups) . ' grups</strong> amb un total de <strong>' . array_sum(array_map('count', $processable_groups)) . ' posts</strong>.</p>';
echo '</div>';
echo '<h3>Primers 10 grups que es vincularan:</h3>';
$count = 0;
foreach ($processable_groups as $tid => $posts) {
if ($count++ >= 10) break;
// Trobar post original
$original_post = null;
foreach ($posts as $p) {
if (strtolower($p->language_code) == $CONFIG['original_lang']) {
$original_post = $p;
break;
}
}
if (!$original_post && !$CONFIG['require_complete_groups']) {
$original_post = $posts[0];
}
$new_trid = min(array_map(fn($p) => $p->current_trid, $posts));
echo '<div class="group">';
echo "<strong>Translation ID: {$tid}</strong> → Nou TRID: <strong>{$new_trid}</strong><br>";
echo '<table><tr><th>ID</th><th>Idioma</th><th>TRID actual</th><th>Nou TRID</th><th>Source Lang</th><th>Acció</th></tr>';
usort($posts, function($a, $b) use ($CONFIG) {
$order = array_flip($CONFIG['languages']);
return ($order[strtolower($a->language_code)] ?? 99) - ($order[strtolower($b->language_code)] ?? 99);
});
foreach ($posts as $p) {
$is_original = ($original_post && $p->ID == $original_post->ID);
$new_source = $is_original ? 'NULL' : $CONFIG['original_lang'];
$action = $is_original ? '<span class="badge badge-success">ORIGINAL</span>' : '<span class="badge badge-info">Traducció</span>';
echo '<tr>';
echo '<td>' . $p->ID . '</td>';
echo '<td><strong>' . strtoupper($p->language_code) . '</strong></td>';
echo '<td>' . $p->current_trid . '</td>';
echo '<td><strong>' . $new_trid . '</strong></td>';
echo '<td>' . $new_source . '</td>';
echo '<td>' . $action . '</td>';
echo '</tr>';
}
echo '</table></div>';
}
if (count($processable_groups) > 10) {
echo '<p><em>... i ' . (count($processable_groups) - 10) . ' grups més</em></p>';
}
echo '<div class="box warning">';
echo '<h3>⚠️ IMPORTANT abans d\'executar:</h3>';
echo '<ol>';
echo '<li><strong>Fes un backup de la base de dades</strong></li>';
echo '<li>Revisa que els exemples són correctes</li>';
echo '<li>Assegura\'t que l\'idioma original és el correcte</li>';
echo '</ol>';
echo '<a href="?key=' . $CONFIG['security_key'] . '&mode=execute" class="btn btn-danger">▶️ EXECUTAR VINCULACIÓ</a> ';
echo '<a href="?key=' . $CONFIG['security_key'] . '&mode=check" class="btn btn-primary">◀️ Tornar</a>';
echo '</div>';
}
// === MODE EXECUCIÓ ===
elseif ($mode == 'execute') {
if (empty($processable_groups)) {
echo '<div class="box error"><strong>❌ No hi ha grups per vincular.</strong></div>';
echo '</div></body></html>';
exit;
}
echo '<div class="box info"><h3>🔄 Executant vinculació...</h3></div>';
$updated = 0;
$errors = 0;
$log = [];
foreach ($processable_groups as $tid => $posts) {
// Trobar post original
$original_post = null;
foreach ($posts as $p) {
if (strtolower($p->language_code) == $CONFIG['original_lang']) {
$original_post = $p;
break;
}
}
if (!$original_post && !$CONFIG['require_complete_groups']) {
$original_post = $posts[0];
}
if (!$original_post) {
$log[] = "⚠️ Grup {$tid}: No s'ha trobat post original, saltat";
continue;
}
$new_trid = min(array_map(fn($p) => $p->current_trid, $posts));
foreach ($posts as $p) {
$is_original = ($p->ID == $original_post->ID);
$result = $wpdb->update(
$wpdb->prefix . 'icl_translations',
[
'trid' => $new_trid,
'source_language_code' => $is_original ? null : $CONFIG['original_lang']
],
[
'element_id' => $p->ID,
'element_type' => 'post_' . $CONFIG['post_type']
]
);
if ($result !== false) {
$updated++;
$log[] = "✓ Post {$p->ID} [{$p->language_code}] → TRID {$new_trid}";
} else {
$errors++;
$log[] = "✗ ERROR: Post {$p->ID} [{$p->language_code}]";
}
}
}
echo '<div class="box success">';
echo '<h3>✅ Vinculació completada!</h3>';
echo "<p>Posts actualitzats: <strong>{$updated}</strong></p>";
echo "<p>Grups vinculats: <strong>" . count($processable_groups) . "</strong></p>";
if ($errors > 0) {
echo "<p style='color:#dc3545'>Errors: <strong>{$errors}</strong></p>";
}
echo '</div>';
if (!empty($log)) {
echo '<div class="box info">';
echo '<h3>📝 Log (primers 30):</h3>';
echo '<pre style="max-height:400px; overflow-y:auto; font-size:11px; background:#2c3e50; color:#ecf0f1; padding:15px; border-radius:5px;">';
foreach (array_slice($log, 0, 30) as $line) {
echo $line . "\n";
}
if (count($log) > 30) {
echo "... i " . (count($log) - 30) . " operacions més\n";
}
echo '</pre>';
echo '</div>';
}
echo '<div class="box info">';
echo '<h3>📋 Següents passos:</h3>';
echo '<ol>';
echo '<li>Comprova al backend que els posts estan vinculats</li>';
echo '<li>Prova el selector d\'idiomes al frontend</li>';
echo '<li>Si tot funciona, <strong>elimina aquest fitxer</strong></li>';
echo '</ol>';
echo '<a href="' . admin_url('edit.php?post_type=' . $CONFIG['post_type']) . '" class="btn btn-success" target="_blank">📝 Anar al backend</a> ';
echo '<a href="?key=' . $CONFIG['security_key'] . '&mode=check" class="btn btn-primary">◀️ Nova verificació</a>';
echo '</div>';
// Netejar cache
if (function_exists('icl_cache_clear')) {
icl_cache_clear();
}
}
?>
<hr>
<p style="text-align:center; color:#7f8c8d; font-size:12px;">
<em>Script genèric de vinculació WPML - Milimetric Marketing - https://milimetricmkt.com</em>
</p>
</div>
</body>
</html>