feat(bricks+wpml): full programmatic page generation + native WPML CTE integration

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>
This commit is contained in:
2026-05-14 19:40:28 +02:00
parent af0ed66122
commit d2badf49d4
28 changed files with 4305 additions and 1 deletions

View File

@@ -0,0 +1,185 @@
<?php
/**
* Plugin Name: Asterion — Create ATE-flagged translate_job rows for our FR pages
* Description:
* /?av_wpml_create_jobs=1 → for each FR linked page, insert a row in
* wp_icl_translate_job with editor='ate' + translated=1, and adjust the
* translation_status row to mirror Bricks #14's pattern (status=2 IN_PROGRESS
* with uuid + batch_id). This should switch the page-list icon from pencil
* ✏️ to gear ⚙️ and route the click to ATE instead of WP editor.
*
* /?av_wpml_remove_jobs=1 → revert if it breaks something (delete jobs, set
* status=10 again).
*
* This is exploratory : we mimic the structure WPML's internal send_jobs()
* creates, without going through TranslationProxy (cloud).
*/
defined('ABSPATH') || exit;
add_action('template_redirect', function () {
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) return;
if (! empty($_GET['av_wpml_create_jobs'])) av_wpml_create_jobs();
if (! empty($_GET['av_wpml_remove_jobs'])) av_wpml_remove_jobs();
});
function av_target_fr_translations() {
global $wpdb;
$slugs = ['industries', 'police', 'special-forces', 'military', 'firefighters',
'solutions', 'my-proserve', 'proserve-flex', 'proserve-academy', 'customization'];
$placeholders = implode(',', array_fill(0, count($slugs), '%s'));
return $wpdb->get_results($wpdb->prepare(
"SELECT p.ID AS post_id, p.post_title AS title, t.trid, t.translation_id, ts.rid, ts.status
FROM {$wpdb->posts} p
JOIN {$wpdb->prefix}icl_translations t ON t.element_id = p.ID AND t.element_type='post_page' AND t.language_code='fr'
LEFT JOIN {$wpdb->prefix}icl_translation_status ts ON ts.translation_id = t.translation_id
WHERE p.post_name IN ($placeholders)",
...$slugs
));
}
function av_wpml_create_jobs() {
global $wpdb;
header('Content-Type: text/plain; charset=utf-8');
$rows = av_target_fr_translations();
if (! $rows) { echo "No FR translations found\n"; exit; }
$batch_id = 1; // reuse Bricks #14's batch (or create new one — using 1 for simplicity)
foreach ($rows as $r) {
if (! $r->rid) {
echo "FR #{$r->post_id} '{$r->title}' : no translation_status row, skipping\n";
continue;
}
// 1. Set translation_status to status=2 (ICL_TM_IN_PROGRESS) to match
// Bricks #14's pattern. This is what makes the FR column icon = gear ⚙️
// and routes the click to the translation editor (CTE).
// Note: editing the EN page directly will show a "Translation in progress,
// wait before editing" warning dialog — informative only, click "I understand"
// to dismiss.
$uuid = wp_generate_uuid4();
$wpdb->update(
$wpdb->prefix . 'icl_translation_status',
[
'status' => 2, // ICL_TM_IN_PROGRESS (= gear icon)
'needs_update' => 0,
'translator_id' => 1,
'translation_service' => 'local',
'batch_id' => $batch_id,
'uuid' => $uuid,
'tp_revision' => 1,
'timestamp' => current_time('mysql'),
'links_fixed' => 1,
],
['rid' => $r->rid]
);
// 2. Insert/update translate_job row with editor='ate'
$existing_job = $wpdb->get_var($wpdb->prepare(
"SELECT job_id FROM {$wpdb->prefix}icl_translate_job WHERE rid=%d ORDER BY job_id DESC LIMIT 1",
$r->rid
));
// editor='wpml' = CTE (Classic Translation Editor), works locally without
// cloud sync. translated=1 + completed_date set = the FR is "ready",
// matches Bricks #14's pattern (which shows gear icon and opens CTE).
// CRITICAL : revision MUST be NULL. WPML's filter_status_css_class() in
// class-wpml-tm-translation-status-display.php joins icl_translate_job
// with the explicit condition `translate_job.revision IS NULL`. Jobs
// with non-null revision are silently ignored → pencil icon, no CTE.
$job_data = [
'rid' => $r->rid,
'translator_id' => 1,
'translated' => 1, // FR considered done (we pre-filled via dict)
'manager_id' => 1,
// 'revision' deliberately omitted → DB default = NULL
'title' => $r->title,
'deadline_date' => null,
'completed_date' => current_time('mysql'),
'editor' => 'wpml', // CTE (matches doc_translation_method=1)
'editor_job_id' => null,
'automatic' => 0,
];
if ($existing_job) {
$wpdb->update($wpdb->prefix . 'icl_translate_job', $job_data, ['job_id' => $existing_job]);
// $wpdb->update can't write SQL NULL (it coerces to empty string).
// Force revision NULL via raw query for the icon's JOIN to match.
$wpdb->query($wpdb->prepare(
"UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE job_id = %d",
$existing_job
));
echo "FR #{$r->post_id} '{$r->title}' (rid={$r->rid}) : job UPDATED #{$existing_job} (editor={$job_data['editor']}, revision=NULL)\n";
} else {
$wpdb->insert($wpdb->prefix . 'icl_translate_job', $job_data);
$new_job_id = $wpdb->insert_id;
// Belt-and-suspenders : ensure revision is NULL even on fresh insert.
$wpdb->query($wpdb->prepare(
"UPDATE {$wpdb->prefix}icl_translate_job SET revision = NULL WHERE job_id = %d",
$new_job_id
));
echo "FR #{$r->post_id} '{$r->title}' (rid={$r->rid}) : job CREATED #{$new_job_id} (editor={$job_data['editor']}, revision=NULL)\n";
}
}
echo "\nDONE. Refresh WP Admin → Pages. The FR column icon should switch from pencil ✏️ to gear ⚙️.\n";
echo "Click the gear to open ATE for that page.\n";
exit;
}
function av_wpml_remove_jobs() {
global $wpdb;
header('Content-Type: text/plain; charset=utf-8');
$rows = av_target_fr_translations();
foreach ($rows as $r) {
if (! $r->rid) continue;
// 1. Remove translate_job rows
$wpdb->query($wpdb->prepare(
"DELETE FROM {$wpdb->prefix}icl_translate_job WHERE rid=%d",
$r->rid
));
// 2. Reset status row : ALL the cloud-sync artifacts cleared
$wpdb->update(
$wpdb->prefix . 'icl_translation_status',
[
'status' => 10,
'needs_update' => 0,
'batch_id' => 0,
'uuid' => '',
'tp_id' => '',
'tp_revision' => 0,
'ts_status' => null,
'review_status' => null,
'ate_comm_retry_count' => 0,
'_prevstate' => '',
'translation_service' => 'local',
'translator_id' => 0,
],
['rid' => $r->rid]
);
echo "FR #{$r->post_id} : job removed, status fully reset\n";
}
// 3. Cleanup orphan translation_batches rows that may trigger cloud-fetch
$orphans = $wpdb->query(
"DELETE FROM {$wpdb->prefix}icl_translation_batches
WHERE batch_name LIKE 'asterion%' OR id NOT IN
(SELECT DISTINCT batch_id FROM {$wpdb->prefix}icl_translation_status WHERE batch_id > 0)"
);
echo "\nOrphan translation_batches rows cleaned : $orphans\n";
// 4. Cleanup orphan translation_downloads rows (background ATE sync queue)
$tbl_downloads = $wpdb->prefix . 'icl_translation_downloads';
if ($wpdb->get_var("SHOW TABLES LIKE '$tbl_downloads'")) {
$cleared = $wpdb->query("DELETE FROM $tbl_downloads");
echo "Cleared $cleared rows from icl_translation_downloads (pending cloud sync queue)\n";
}
echo "\nReverted to clean state. WPML queue + page list should be back to normal.\n";
exit;
}