Sessions 3-4 deliverables. Establishes the Asterion content pipeline :
EN pages generated programmatically from content-source/*-data.php, FR
translations rebuilt natively by Bricks's WPML integration from EN structure
+ WPML strings (no direct FR _bricks_page_content_2 writes).
## tools/_av-bricks-shared.php (the library)
Shared helpers + element atoms (eyebrow/heading/text/button/text-link) +
section builders (hero/prose/list/cards/cta/upsell/closer) + page write
pipeline that handles : write EN content via $wpdb (bypass Bricks's filter),
force Bricks::Elements::load_elements() so controls are populated, fire
wpml_page_builder_register_strings, push dict FR into icl_string_translations
(skip user CTE edits, auto-prefix /fr on internal URLs), trigger Bricks
native FR rebuild via wpml_page_builder_string_translated, and ensure a CTE
job exists (icl_translate_job.editor='wpml' + revision=NULL + populate
icl_translate rows with base64+gzip-encoded EN/FR strings).
## tools/av-generate-{industries,solutions}.php
Thin wrappers : URL trigger (template_redirect, not init — Bricks elements
load on 'wp' hook), add_filter('av_fr_translation_dict', ...) to register
FR strings, content-type-specific hub builder. ~150 FR strings per type.
## tools/av-translate-{industries,solutions}-fr.php
One-shot seeds for the initial FR linked posts via WPML trid mapping.
## tools/av-wpml-* operational mu-plugins
- set-cte : switch doc_translation_method to ICL_TM_TMETHOD_EDITOR (=1)
- create-jobs : reset CTE jobs (status=10 + needs_update=1 + editor=wpml +
revision=NULL). Integrated into pipeline via av_ensure_wpml_cte_job().
- fix-status / repair : align FR posts to correct trid + cleanup orphans.
- push-translations : push dict into icl_string_translations.
- plugins-check / debug / dump-strings / icl-dump / rescan : diagnostics.
## tools/av-fix-link-types.php
One-shot DB migration : convert legacy link.type='internal'+url to 'external'
across all _bricks_page_*_2 meta values (JSON and PHP-serialized). Bricks's
set_link_attributes() only renders href= for type='external' — older session-3
templates lost their href silently.
## Other tools/ mu-plugins
- av-bricks-rest-bridge : pivot — exposes Bricks CPTs over REST, dual-format
read filter (JSON for MCP / array for renderer), auto-mirror content→header/
footer for templates.
- av-apply-bricks-styles, av-cleanup-orphans, av-create-wp-menu, av-debug-meta,
av-dump-bricks, av-fix-template-meta : misc operational scripts.
## Bricks Config/
JSON snapshots of bricks_theme_styles, bricks_color_palette,
bricks_global_settings for diffable inspection.
## .gitignore
Add .mcp.json — holds per-developer WP App Password for the Bricks MCP server.
## wp-content/themes/asterion-bricks/assets/css/utilities.css
Note that all responsive overrides are now emitted natively by the page
generator via Bricks per-breakpoint settings, no custom CSS needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
271 lines
12 KiB
PHP
271 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Asterion — WPML translation_status diagnose + fix
|
|
* Description:
|
|
* /?av_wpml_status_dump=1 → dump state of every linked FR page (icl_translations
|
|
* + icl_translation_status + Bricks meta + ATE editor pref)
|
|
* /?av_wpml_status_fix=1 → insert/update icl_translation_status rows so WPML
|
|
* marks our programmatically-translated pages as
|
|
* "completed via ATE", clears the orange "needs update"
|
|
* icon, and lets the pencil → ATE button work.
|
|
*
|
|
* Why : our pipeline writes _bricks_page_content_2 directly via $wpdb. That bypasses
|
|
* WPML's normal "translation completed" hook. WPML sees the linked posts in
|
|
* icl_translations but icl_translation_status is empty → the WPML UI thinks the
|
|
* translation is missing/incomplete and routes the pencil button to the WP editor
|
|
* fallback instead of the Advanced Translation Editor.
|
|
*/
|
|
defined('ABSPATH') || exit;
|
|
|
|
add_action('init', function () {
|
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
|
|
|
|
if (! empty($_GET['av_wpml_status_dump'])) {
|
|
av_wpml_status_dump();
|
|
}
|
|
if (! empty($_GET['av_wpml_status_fix'])) {
|
|
av_wpml_status_fix();
|
|
}
|
|
if (! empty($_GET['av_wpml_repair'])) {
|
|
av_wpml_repair(); // full pipeline : relink-trid → cleanup orphans → status_fix
|
|
}
|
|
});
|
|
|
|
function av_wpml_target_trids() {
|
|
global $wpdb;
|
|
// All trids that have a /fr/ post WHERE EN parent is /industries/, /solutions/
|
|
// or the hub pages themselves.
|
|
return $wpdb->get_col(
|
|
"SELECT DISTINCT t.trid
|
|
FROM {$wpdb->prefix}icl_translations t
|
|
JOIN {$wpdb->posts} p ON p.ID = t.element_id
|
|
WHERE t.element_type='post_page'
|
|
AND t.element_id > 0
|
|
AND (p.post_name IN ('industries','solutions','police','special-forces','military','firefighters','my-proserve','proserve-flex','proserve-academy','customization'))"
|
|
);
|
|
}
|
|
|
|
function av_wpml_status_dump() {
|
|
global $wpdb;
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
|
|
// 1) Sitepress editor preference
|
|
$opts = get_option('icl_sitepress_settings');
|
|
$editor_default = $opts['translation-management']['doc_translation_method'] ?? '(unset)';
|
|
$editor_alt = $opts['translation_method'] ?? '(unset)';
|
|
$ate_enabled = $opts['translation-management']['ate']['ate_enabled'] ?? '(unset)';
|
|
echo "WPML editor preference :\n";
|
|
echo " doc_translation_method = $editor_default\n";
|
|
echo " translation_method = $editor_alt\n";
|
|
echo " ate_enabled = $ate_enabled\n";
|
|
echo "\n";
|
|
|
|
// 2) For each target trid, dump rows
|
|
$trids = av_wpml_target_trids();
|
|
if (! $trids) { echo "(no target trids found)\n"; exit; }
|
|
echo "Found " . count($trids) . " target trid(s) : " . implode(', ', $trids) . "\n\n";
|
|
|
|
foreach ($trids as $trid) {
|
|
echo "── trid $trid ──\n";
|
|
$tr_rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT translation_id, element_id, language_code, source_language_code
|
|
FROM {$wpdb->prefix}icl_translations
|
|
WHERE trid=%d ORDER BY language_code",
|
|
$trid
|
|
));
|
|
foreach ($tr_rows as $r) {
|
|
$title = get_the_title($r->element_id);
|
|
echo " [{$r->language_code}] post #{$r->element_id} '{$title}' (translation_id={$r->translation_id}, src={$r->source_language_code})\n";
|
|
|
|
// status row
|
|
$st = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT status, needs_update, translation_service, translator_id, md5
|
|
FROM {$wpdb->prefix}icl_translation_status
|
|
WHERE translation_id=%d",
|
|
$r->translation_id
|
|
));
|
|
if ($st) {
|
|
echo " status_row : status=$st->status needs_update=$st->needs_update service=$st->translation_service translator=$st->translator_id md5=" . substr($st->md5 ?? '', 0, 8) . "\n";
|
|
} else {
|
|
echo " status_row : MISSING (no row in icl_translation_status)\n";
|
|
}
|
|
}
|
|
echo "\n";
|
|
}
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Full repair pipeline :
|
|
* 1. For each known EN slug, find EN's trid.
|
|
* 2. Find any FR post with the same slug. If it's linked to a DIFFERENT trid,
|
|
* relink it to EN's trid (this fixes session-3 re-creation cases where
|
|
* set_element_language_details fell back to a fresh trid because an orphan
|
|
* empty FR row blocked the canonical (trid, fr, ...) slot).
|
|
* 3. Delete orphan FR rows (element_id NULL/0/empty) in the canonical trid.
|
|
* 4. Reset icl_translation_status (status=10, needs_update=0, md5 from EN content).
|
|
*/
|
|
function av_wpml_repair() {
|
|
global $wpdb;
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
|
|
$slugs = [
|
|
// Industries
|
|
'industries', 'police', 'special-forces', 'military', 'firefighters',
|
|
// Solutions
|
|
'solutions', 'my-proserve', 'proserve-flex', 'proserve-academy', 'customization',
|
|
];
|
|
|
|
echo "── STEP 1+2 : align FR posts to EN's trid ──\n\n";
|
|
foreach ($slugs as $slug) {
|
|
// EN post for slug
|
|
$en = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT p.ID, t.trid
|
|
FROM {$wpdb->posts} p
|
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page'
|
|
WHERE p.post_name=%s AND t.language_code='en'
|
|
LIMIT 1",
|
|
$slug
|
|
));
|
|
if (! $en) { echo " $slug : no EN page found — skip\n"; continue; }
|
|
|
|
// FR post for slug (regardless of current trid)
|
|
$fr = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT p.ID, t.trid, t.translation_id
|
|
FROM {$wpdb->posts} p
|
|
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page'
|
|
WHERE p.post_name=%s AND t.language_code='fr'
|
|
LIMIT 1",
|
|
$slug
|
|
));
|
|
if (! $fr) { echo " $slug : EN #{$en->ID} (trid={$en->trid}) — no FR yet\n"; continue; }
|
|
|
|
if ((int) $fr->trid === (int) $en->trid) {
|
|
echo " $slug : EN #{$en->ID} ↔ FR #{$fr->ID} aligned on trid={$en->trid} ✓\n";
|
|
} else {
|
|
// 1) Remove any orphan FR row in EN's canonical trid
|
|
$orphans = $wpdb->query($wpdb->prepare(
|
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
|
WHERE trid=%d AND language_code='fr' AND element_type='post_page'
|
|
AND (element_id IS NULL OR element_id = 0 OR element_id = '')",
|
|
$en->trid
|
|
));
|
|
// 2) Move FR translation_id to EN's trid
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'icl_translations',
|
|
['trid' => $en->trid, 'source_language_code' => 'en'],
|
|
['translation_id' => $fr->translation_id],
|
|
['%d', '%s'],
|
|
['%d']
|
|
);
|
|
// 3) Delete the now-empty old trid row(s) of FR's previous trid (in case anything else was attached there)
|
|
// Skip — keep it conservative. The old trid row for FR is now moved, not deleted.
|
|
|
|
echo " $slug : FR #{$fr->ID} moved from trid={$fr->trid} → {$en->trid} (deleted $orphans orphan(s))\n";
|
|
}
|
|
}
|
|
|
|
echo "\n── STEP 3 : remove ALL orphan FR rows across known trids ──\n\n";
|
|
$trids = av_wpml_target_trids();
|
|
foreach ($trids as $trid) {
|
|
$deleted = $wpdb->query($wpdb->prepare(
|
|
"DELETE FROM {$wpdb->prefix}icl_translations
|
|
WHERE trid=%d AND (element_id IS NULL OR element_id = 0 OR element_id = '')",
|
|
$trid
|
|
));
|
|
if ($deleted > 0) echo " trid $trid : deleted $deleted orphan row(s)\n";
|
|
}
|
|
|
|
echo "\n── STEP 4 : write icl_translation_status (status=10 completed) ──\n\n";
|
|
av_wpml_status_fix(false);
|
|
echo "\nDONE.\n";
|
|
exit;
|
|
}
|
|
|
|
function av_wpml_status_fix($exit_after = true) {
|
|
global $wpdb;
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
|
|
$trids = av_wpml_target_trids();
|
|
$patched = 0; $inserted = 0;
|
|
|
|
foreach ($trids as $trid) {
|
|
// Get EN and FR translation_id + post_id
|
|
$en = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT translation_id, element_id FROM {$wpdb->prefix}icl_translations
|
|
WHERE trid=%d AND language_code='en' AND element_id > 0 LIMIT 1",
|
|
$trid
|
|
));
|
|
$fr = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT translation_id, element_id FROM {$wpdb->prefix}icl_translations
|
|
WHERE trid=%d AND language_code='fr' AND element_id > 0 LIMIT 1",
|
|
$trid
|
|
));
|
|
if (! $en || ! $fr) {
|
|
echo "trid $trid : EN or FR missing — skip\n";
|
|
continue;
|
|
}
|
|
|
|
// Compute md5 of EN _bricks_page_content_2 (what WPML's translation engine would
|
|
// hash on a normal flow). When EN changes, md5 differs → WPML shows "needs update".
|
|
$en_content = get_post_meta($en->element_id, '_bricks_page_content_2', true);
|
|
if (is_array($en_content)) $en_content = wp_json_encode($en_content);
|
|
$md5 = md5((string) $en_content);
|
|
|
|
// Status codes (WPML constants, hardcoded for portability) :
|
|
// ICL_TM_NOT_TRANSLATED = 0
|
|
// ICL_TM_WAITING = 1
|
|
// ICL_TM_NEEDS_UPDATE = 3
|
|
// ICL_TM_DUPLICATE = 9
|
|
// ICL_TM_COMPLETE = 10
|
|
$data = [
|
|
'translation_id' => $fr->translation_id,
|
|
'status' => 10, // ICL_TM_COMPLETE
|
|
'needs_update' => 0,
|
|
'md5' => $md5,
|
|
'translation_service' => 'local', // not delegated to a paid service
|
|
'translator_id' => 0, // local human (us, programmatic)
|
|
'translated_by_id' => 0,
|
|
'links_fixed' => 1,
|
|
];
|
|
|
|
$existing = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT rid FROM {$wpdb->prefix}icl_translation_status WHERE translation_id=%d",
|
|
$fr->translation_id
|
|
));
|
|
|
|
if ($existing) {
|
|
// Only update fields that exist (drop optional ones to be safe)
|
|
$update = $data;
|
|
unset($update['translation_id']); // not changing PK
|
|
$wpdb->update($wpdb->prefix . 'icl_translation_status', $update, ['rid' => $existing]);
|
|
echo "trid $trid : FR #{$fr->element_id} → status row UPDATED (rid=$existing, status=10, md5=" . substr($md5, 0, 8) . ")\n";
|
|
$patched++;
|
|
} else {
|
|
// Be careful : the table may not have all columns. Try with full first, fallback.
|
|
$ok = $wpdb->insert($wpdb->prefix . 'icl_translation_status', $data);
|
|
if (! $ok) {
|
|
// Retry minimal
|
|
$minimal = [
|
|
'translation_id' => $fr->translation_id,
|
|
'status' => 10,
|
|
'needs_update' => 0,
|
|
'md5' => $md5,
|
|
];
|
|
$ok = $wpdb->insert($wpdb->prefix . 'icl_translation_status', $minimal);
|
|
}
|
|
if ($ok) {
|
|
echo "trid $trid : FR #{$fr->element_id} → status row INSERTED (status=10, md5=" . substr($md5, 0, 8) . ")\n";
|
|
$inserted++;
|
|
} else {
|
|
echo "trid $trid : FR #{$fr->element_id} → INSERT FAILED : " . $wpdb->last_error . "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
echo "\nDONE. patched=$patched, inserted=$inserted\n";
|
|
echo "Refresh WP Admin → Pages — orange refresh icons should clear, pencils should\n";
|
|
echo "route to WPML's editor (ATE if ate_enabled=1, classic editor otherwise).\n";
|
|
if ($exit_after) exit;
|
|
}
|