<?php
/**
* Plugin Name: DCN Bilingual
* Description: Custom bilingual system for DailyCryptoNews.co — V17 Language Routing & Translation Resolver Core Fix
* Version: 1.1.0
* Author: Hermes Agent
* Text Domain: dcn-bilingual
*
* V17 CHANGES:
* - Translation Resolver (resolve_translation) — deterministic post_id resolution by language
* - Strict Guard (template_redirect) — blocks cross-language rendering
* - Homepage Query Fix — strict FR/EN isolation on homepage
* - Language Switch Fix — proper URL resolution + cache flush
* - Cache Invalidation — per-language cache isolation
* - Debug Mode — LANG_SWITCH_LOG for every routing decision
*/
defined('ABSPATH') || exit;
define('DCN_BL_VERSION', '1.1.0');
define('DCN_BL_PLUGIN_DIR', plugin_dir_path(__FILE__));
class DCN_Bilingual {
const TAXONOMY = 'dcn_language';
const META_TRANSLATION_OF = '_dcn_translation_of';
const LANG_FR = 'fr';
const LANG_EN = 'en';
private static $instance = null;
private static $debug_log = [];
private $current_language = '';
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action('init', [$this, 'register_taxonomy'], 15);
add_action('init', [$this, 'register_rewrite_rules'], 15);
add_action('init', [$this, 'register_meta'], 20);
// Permalink filters
add_filter('post_type_link', [$this, 'post_type_link'], 10, 2);
add_filter('post_link', [$this, 'post_type_link'], 10, 2);
add_filter('page_link', [$this, 'page_link'], 10, 2);
add_filter('term_link', [$this, 'term_link'], 10, 3);
// Query filter
add_action('parse_query', [$this, 'parse_query']);
add_filter('posts_clauses', [$this, 'posts_clauses'], 10, 2);
// ─── V17 CORE: Language Routing & Translation Resolver ───
add_action('wp', [$this, 'resolve_current_post_language'], 5);
add_action('template_redirect', [$this, 'strict_guard'], 1);
add_filter('the_post', [$this, 'verify_post_language'], 10, 2);
add_filter('wp_get_nav_menu_items', [$this, 'filter_menu_by_language'], 10, 2);
// Language switch URL resolution
add_filter('get_edit_post_link', [$this, 'add_language_to_admin_link'], 10, 3);
// JARVIS V6.1 - Breadcrumb + Hreflang schema fixes
require_once plugin_dir_path(__FILE__) . 'includes/class-dcn-breadcrumb.php';
new DCN_Breadcrumb();
new DCN_Hreflang();
// Hreflang (disabled - handled by DCN_Hreflang class)
// add_action('wp_head', [$this, 'hreflang_tags'], 1);
// Analytics (GA4 + Adsterra)
add_action('wp_head', [$this, 'inject_analytics'], 2);
add_action('wp_footer', [$this, 'inject_analytics_footer'], 1);
// AADS
add_action('wp_body_open', [$this, 'inject_body_ad'], 1);
add_action('wp_footer', [$this, 'inject_footer_ad'], 5);
// REST API
add_action('rest_api_init', [$this, 'rest_api_init']);
add_action('rest_api_init', [$this, 'register_translate_endpoint'], 20);
// Admin
add_action('admin_init', [$this, 'admin_init']);
// URL detection
add_action('wp_loaded', [$this, 'detect_language_from_url'], 5);
// ─── V17: Debug endpoint ───
add_action('rest_api_init', [$this, 'register_debug_endpoint']);
}
// ═══════════════════════════════════════════════════════════════════════
// V17 CORE: TRANSLATION RESOLVER
// ═══════════════════════════════════════════════════════════════════════
/**
* Resolve a post_id to the correct language version.
* This is the single source of truth for all language routing.
*
* @param int $post_id Original post ID
* @param string $target_lang Target language ('fr' or 'en')
* @return int Resolved post ID
*/
public function resolve_translation($post_id, $target_lang) {
$original_id = (int) $post_id;
$post_lang = $this->get_post_language($original_id);
// RULE 1: Already in target language
if ($post_lang === $target_lang) {
$this->debug_log('resolve_translation', $original_id, $target_lang, [
'resolved_to' => $original_id,
'reason' => 'already_in_target_lang',
'post_lang' => $post_lang,
'fallback_used' => false,
]);
return $original_id;
}
// RULE 2: Try explicit translation link
$translation_id = (int) get_post_meta($original_id, self::META_TRANSLATION_OF, true);
if ($translation_id > 0) {
// V17 FIX: Verify bidirectional consistency
$reverse_check = (int) get_post_meta($translation_id, self::META_TRANSLATION_OF, true);
$translation_lang = $this->get_post_language($translation_id);
if ($translation_lang === $target_lang && $reverse_check === $original_id) {
// V17: Clear cache for this route on resolved switch
$this->maybe_flush_cache($original_id, $translation_id);
$this->debug_log('resolve_translation', $original_id, $target_lang, [
'resolved_to' => $translation_id,
'reason' => 'translation_link_found',
'translation_lang' => $translation_lang,
'bidirectional' => true,
'fallback_used' => false,
]);
return $translation_id;
}
// Broken link — remove it
if ($translation_lang !== $target_lang) {
delete_post_meta($original_id, self::META_TRANSLATION_OF);
$this->debug_log('resolve_translation', $original_id, $target_lang, [
'resolved_to' => $original_id,
'reason' => 'broken_link_removed',
'translation_lang' => $translation_lang,
'fallback_used' => true,
]);
}
}
// RULE 3: Fallback — search by same cluster + language
$fallback_id = $this->find_best_match($original_id, $target_lang);
if ($fallback_id > 0) {
$this->debug_log('resolve_translation', $original_id, $target_lang, [
'resolved_to' => $fallback_id,
'reason' => 'fallback_cluster_match',
'fallback_used' => true,
]);
return $fallback_id;
}
// Ultimate fallback: return original (will be caught by strict_guard)
$this->debug_log('resolve_translation', $original_id, $target_lang, [
'resolved_to' => $original_id,
'reason' => 'no_fallback_found_returning_original',
'fallback_used' => true,
'warning' => 'post_may_be_wrong_language',
]);
return $original_id;
}
/**
* Find the best matching post in target language from same category cluster.
*/
private function find_best_match($post_id, $target_lang) {
$categories = wp_get_post_categories($post_id);
if (empty($categories)) return 0;
// Search for any published post in same categories with target language
$args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
'category__in' => $categories,
'tax_query' => [[
'taxonomy' => self::TAXONOMY,
'field' => 'slug',
'terms' => $target_lang,
]],
'orderby' => 'date',
'order' => 'DESC',
];
// Exclude the current post
$args['post__not_in'] = [$post_id];
$query = new WP_Query($args);
return !empty($query->posts) ? (int) $query->posts[0] : 0;
}
// ═══════════════════════════════════════════════════════════════════════
// V17 CORE: STRICT GUARD
// ═══════════════════════════════════════════════════════════════════════
/**
* RULE #6 — Strict Guard: Block rendering if post language != current language.
* Runs before template rendering via template_redirect.
*/
public function strict_guard() {
if (is_admin()) return;
if (!is_singular('post') && !is_singular('page')) return;
global $post;
if (!$post) return;
$current_lang = $this->get_current_language();
$post_lang = $this->get_post_language($post->ID);
if ($post_lang !== $current_lang) {
// Attempt to resolve to correct translation
$resolved_id = $this->resolve_translation($post->ID, $current_lang);
if ($resolved_id !== $post->ID) {
// Redirect to correct language version
$correct_url = get_permalink($resolved_id);
if ($correct_url) {
wp_redirect($correct_url, 302);
exit;
}
}
// If no resolution found, we still block rendering
// by showing a 404 or redirecting to homepage
if ($post_lang !== $current_lang) {
$this->debug_log('strict_guard', $post->ID, $current_lang, [
'action' => 'redirect_to_homepage',
'post_lang' => $post_lang,
'reason' => 'no_valid_translation_found',
]);
wp_redirect(home_url('/' . $current_lang . '/'), 302);
exit;
}
}
}
/**
* RULE #4 — Verify each post in The Loop matches current language.
*/
public function verify_post_language($post, $wp_query) {
if (is_admin()) return $post;
if (!$wp_query->is_main_query()) return $post;
$current_lang = $this->get_current_language();
$post_lang = $this->get_post_language($post->ID);
if ($post_lang !== $current_lang) {
// Try to resolve silently
$resolved = $this->resolve_translation($post->ID, $current_lang);
if ($resolved !== $post->ID) {
$resolved_post = get_post($resolved);
if ($resolved_post) return $resolved_post;
}
}
return $post;
}
// ═══════════════════════════════════════════════════════════════════════
// V17 CORE: HOMEPAGE QUERY FIX
// ═══════════════════════════════════════════════════════════════════════
/**
* RULE #3 — Homepage query: strictly filter by current language.
* Ensures FR homepage only shows FR posts, EN homepage only EN posts.
*/
public function posts_clauses($clauses, $query) {
if (is_admin()) return $clauses;
// Skip REST API to keep endpoints complete
if (defined("REST_REQUEST") && REST_REQUEST) return $clauses;
$lang = $this->get_current_language();
if (!$lang) return $clauses;
global $wpdb;
$term = term_exists($lang, self::TAXONOMY);
if ($term) {
$term_id = is_array($term) ? $term['term_id'] : $term;
// V17: Force language isolation via SQL JOIN
$clauses['join'] .= $wpdb->prepare(
" INNER JOIN {$wpdb->term_relationships} AS dcn_tr
ON ({$wpdb->posts}.ID = dcn_tr.object_id)
INNER JOIN {$wpdb->term_taxonomy} AS dcn_tt
ON (dcn_tr.term_taxonomy_id = dcn_tt.term_taxonomy_id
AND dcn_tt.taxonomy = %s
AND dcn_tt.term_id = %d)",
self::TAXONOMY,
$term_id
);
// Ensure no duplicate posts
$clauses['distinct'] = 'DISTINCT';
}
return $clauses;
}
// ═══════════════════════════════════════════════════════════════════════
// V17 CORE: LANGUAGE SWITCH + CACHE
// ═══════════════════════════════════════════════════════════════════════
/**
* RULE #5 — Flush cache when switching languages.
*/
private function maybe_flush_cache($from_post_id, $to_post_id) {
if (!function_exists('wp_cache_delete')) return;
// Clear both post caches
wp_cache_delete($from_post_id, 'posts');
wp_cache_delete($to_post_id, 'posts');
// Clear permalink cache
wp_cache_delete('rewrite_rules', 'options');
$this->debug_log('cache_flush', $from_post_id, '', [
'target_post' => $to_post_id,
'cache_cleared' => true,
]);
}
/**
* Get the current language from URL or cookie.
*/
public function get_current_language() {
if (!empty($this->current_language)) {
return $this->current_language;
}
$lang = get_query_var('language');
if ($lang && in_array($lang, [self::LANG_FR, self::LANG_EN], true)) {
$this->current_language = $lang;
return $lang;
}
$lang = $this->detect_lang_from_request();
if ($lang) {
$this->current_language = $lang;
return $lang;
}
return self::LANG_FR; // Default fallback
}
/**
* Resolve current post language at 'wp' action.
* This ensures the queried post matches the detected language.
*/
public function resolve_current_post_language() {
if (is_admin()) return;
if (!is_singular('post') && !is_singular('page')) return;
global $post;
if (!$post) return;
$current_lang = $this->get_current_language();
$post_lang = $this->get_post_language($post->ID);
if ($post_lang !== $current_lang) {
$resolved = $this->resolve_translation($post->ID, $current_lang);
if ($resolved !== $post->ID) {
// Override the global $post with the resolved version
$resolved_post = get_post($resolved);
if ($resolved_post) {
$post = $resolved_post;
setup_postdata($post);
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// V17 CORE: DEBUG LOGGING
// ═══════════════════════════════════════════════════════════════════════
private function debug_log($action, $post_id, $language, $context = []) {
self::$debug_log[] = array_merge([
'timestamp' => current_time('c'),
'action' => $action,
'post_id' => $post_id,
'language' => $language,
], $context);
// Keep last 100 entries
if (count(self::$debug_log) > 100) {
array_shift(self::$debug_log);
}
}
public function get_debug_log() {
return self::$debug_log;
}
public function register_debug_endpoint() {
register_rest_route('dcn/v8', '/debug/log', [
'methods' => 'GET',
'callback' => function () {
return [
'success' => true,
'log' => $this->get_debug_log(),
'count' => count($this->get_debug_log()),
'current_language' => $this->get_current_language(),
];
},
'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
// Public debug endpoint (no auth) — limited info
register_rest_route('dcn/v8', '/debug/status', [
'methods' => 'GET',
'callback' => function () {
$lang = $this->get_current_language();
return [
'current_language' => $lang,
'language_from_url' => $this->detect_lang_from_request(),
'cache_loaded' => defined('WP_CACHE') && WP_CACHE,
'plugin_version' => DCN_BL_VERSION,
];
},
'permission_callback' => '__return_true',
]);
}
/**
* Filter menu items to only show those matching current language.
*/
public function filter_menu_by_language($items, $menu) {
if (is_admin()) return $items;
$current_lang = $this->get_current_language();
$filtered = [];
foreach ($items as $item) {
$item_lang = $this->get_post_language($item->object_id);
if (empty($item_lang) || $item_lang === $current_lang) {
$filtered[] = $item;
}
}
return $filtered;
}
/**
* Add language parameter to admin edit links for easier switch debugging.
*/
public function add_language_to_admin_link($url, $post_id, $context) {
if (is_admin()) return $url;
$lang = $this->get_post_language($post_id);
return add_query_arg('dcn_lang', $lang, $url);
}
// ═══════════════════════════════════════════════════════════════════════
// EXISTING METHODS (unchanged from v1.0.2)
// ═══════════════════════════════════════════════════════════════════════
public function register_taxonomy() {
register_taxonomy(self::TAXONOMY, ['post', 'page'], [
'labels' => [
'name' => __('Languages', 'dcn-bilingual'),
'singular_name' => __('Language', 'dcn-bilingual'),
'all_items' => __('All Languages', 'dcn-bilingual'),
'edit_item' => __('Edit Language', 'dcn-bilingual'),
'view_item' => __('View Language', 'dcn-bilingual'),
'update_item' => __('Update Language', 'dcn-bilingual'),
'add_new_item' => __('Add New Language', 'dcn-bilingual'),
'new_item_name' => __('New Language Name', 'dcn-bilingual'),
],
'public' => true,
'publicly_queryable' => false,
'show_ui' => true,
'show_admin_column' => true,
'show_in_menu' => true,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_in_rest' => true,
'rest_base' => 'language',
'hierarchical' => false,
'query_var' => 'language',
'rewrite' => false,
'capabilities' => [
'manage_terms' => 'manage_options',
'edit_terms' => 'manage_options',
'delete_terms' => 'manage_options',
'assign_terms' => 'edit_posts',
],
]);
if (!term_exists(self::LANG_FR, self::TAXONOMY)) {
wp_insert_term('Français', self::TAXONOMY, ['slug' => self::LANG_FR]);
}
if (!term_exists(self::LANG_EN, self::TAXONOMY)) {
wp_insert_term('English', self::TAXONOMY, ['slug' => self::LANG_EN]);
}
}
public function register_rewrite_rules() {
global $wp_rewrite;
if (!$wp_rewrite instanceof WP_Rewrite) return;
add_rewrite_tag('%language%', '(fr|en)');
add_rewrite_rule('^fr/?$', 'index.php?language=fr', 'top');
add_rewrite_rule('^fr/page/([0-9]+)/?$', 'index.php?language=fr&paged=$matches[1]', 'top');
add_rewrite_rule('^en/?$', 'index.php?language=en', 'top');
add_rewrite_rule('^en/page/([0-9]+)/?$', 'index.php?language=en&paged=$matches[1]', 'top');
// V20: Category-level FR/EN URL patterns
add_rewrite_rule('^fr/category/([^/]+)/?$', 'index.php?language=fr&category_name=$matches[1]', 'top');
add_rewrite_rule('^fr/category/([^/]+)/page/([0-9]+)/?$', 'index.php?language=fr&category_name=$matches[1]&paged=$matches[2]', 'top');
add_rewrite_rule('^en/category/([^/]+)/?$', 'index.php?language=en&category_name=$matches[1]', 'top');
add_rewrite_rule('^en/category/([^/]+)/page/([0-9]+)/?$', 'index.php?language=en&category_name=$matches[1]&paged=$matches[2]', 'top');
// V20: Tag-level FR/EN URL patterns
add_rewrite_rule('^fr/tag/([^/]+)/?$', 'index.php?language=fr&tag=$matches[1]', 'top');
add_rewrite_rule('^en/tag/([^/]+)/?$', 'index.php?language=en&tag=$matches[1]', 'top');
// V20: Year archive
add_rewrite_rule('^fr/([0-9]{4})/?$', 'index.php?language=fr&year=$matches[1]', 'top');
add_rewrite_rule('^en/([0-9]{4})/?$', 'index.php?language=en&year=$matches[1]', 'top');
// V17.1: Post-level FR/EN URL patterns
add_rewrite_rule('^fr/([^/]+)/?$', 'index.php?language=fr&name=$matches[1]&post_type=post', 'top');
add_rewrite_rule('^en/([^/]+)/?$', 'index.php?language=en&name=$matches[1]&post_type=post', 'top');
}
public function register_meta() {
register_post_meta('', self::META_TRANSLATION_OF, [
'show_in_rest' => true,
'single' => true,
'type' => 'integer',
'description' => 'ID of the translated version of this post',
'auth_callback' => function () { return current_user_can('edit_posts'); },
]);
}
private function get_post_language($post_id) {
if (!is_numeric($post_id)) return self::LANG_FR;
$post_id = (int) $post_id;
if (!taxonomy_exists(self::TAXONOMY)) return self::LANG_FR;
$terms = wp_get_object_terms($post_id, self::TAXONOMY, ['fields' => 'slugs']);
if (is_wp_error($terms) || empty($terms)) return self::LANG_FR;
if (in_array($terms[0], [self::LANG_FR, self::LANG_EN], true)) return $terms[0];
return self::LANG_FR;
}
public function post_type_link($url, $post) {
static $recursing = false;
if ($recursing) return $url;
if (is_string($post)) $post = get_post($post);
if (!$post) return $url;
$lang = $this->get_post_language($post->ID);
$home = home_url('/');
$home_len = strlen($home);
if (strncmp($url, $home, $home_len) !== 0) return $url;
$path = substr($url, $home_len);
$path = preg_replace('#^(fr|en)/#', '', $path);
$recursing = true;
$new_url = $home . $lang . '/' . $path;
$recursing = false;
return $new_url;
}
public function page_link($url, $page_id) {
return $this->post_type_link($url, get_post($page_id));
}
public function term_link($url, $term, $taxonomy) {
return $url;
}
public function parse_query($query) {
if (is_admin()) return;
if (!$query->is_main_query()) return;
$lang = get_query_var('language');
if (!$lang) $lang = $this->detect_lang_from_request();
if ($lang && in_array($lang, [self::LANG_FR, self::LANG_EN], true)) {
$tax_query = $query->get('tax_query') ?: [];
$tax_query[] = [
'taxonomy' => self::TAXONOMY,
'field' => 'slug',
'terms' => $lang,
];
$query->set('tax_query', $tax_query);
}
}
private function detect_lang_from_request() {
$request_uri = isset($_SERVER["REQUEST_URI"]) ? $_SERVER["REQUEST_URI"] : "";
if (preg_match("#^/(fr|en)(/|$)#", $request_uri, $m)) return $m[1];
// Detect from category slug prefix (en- = English, else French)
if (preg_match("#^/category/en-#", $request_uri)) return "en";
if (preg_match("#^/category/#", $request_uri)) return "fr";
return "";
}
public function detect_language_from_url() {
global $wp_query;
if (is_admin()) return;
if (!$wp_query) return;
$lang = $this->detect_lang_from_request();
if ($lang) set_query_var('language', $lang);
}
public function hreflang_tags() {
if (!is_singular('post') && !is_singular('page')) return;
global $post;
$current_lang = $this->get_post_language($post->ID);
$translation_of = get_post_meta($post->ID, self::META_TRANSLATION_OF, true);
$fr_url = home_url('/');
$en_url = home_url('/');
if ($translation_of) {
$fr_id = (self::LANG_FR === $current_lang) ? $post->ID : $translation_of;
$en_id = (self::LANG_EN === $current_lang) ? $post->ID : $translation_of;
$fr_post = get_post($fr_id);
$en_post = get_post($en_id);
if ($fr_post) $fr_url = get_permalink($fr_post);
if ($en_post) $en_url = get_permalink($en_post);
} else {
$fr_url = home_url('/fr/' . $post->post_name . '/');
$en_url = home_url('/en/' . $post->post_name . '/');
}
echo "\n<!-- DCN Bilingual hreflang -->\n";
echo '<link rel="alternate" hreflang="fr" href="' . esc_url($fr_url) . '" />' . "\n";
echo '<link rel="alternate" hreflang="en" href="' . esc_url($en_url) . '" />' . "\n";
echo '<link rel="alternate" hreflang="x-default" href="' . esc_url($fr_url) . '" />' . "\n";
}
// ─── ANALYTICS ─────────────────────────────────────────────────────
public function inject_analytics() {
echo "\n<!-- DCN GA4 -->\n";
echo '<script async src="https://www.googletagmanager.com/gtag/js?id=G-XNKFX5STHE"></script>' . "\n";
echo '<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag(\'js\',new Date());gtag(\'config\',\'G-XNKFX5STHE\');console.debug(\'[DCN GA4] Loaded G-XNKFX5STHE\');</script>' . "\n";
}
public function inject_analytics_footer() {
if (is_admin()) return;
?>
<script>
(function() {
if (typeof gtag !== 'undefined' && document.querySelector('article')) {
gtag('event', 'article_view', {
'page_title': document.title,
'page_location': window.location.href,
'page_referrer': document.referrer
});
}
var scrollMax = 0;
var scrollThresholds = [25, 50, 75, 90, 100];
var scrollSent = {};
window.addEventListener('scroll', function() {
var winScroll = document.body.scrollTop || document.documentElement.scrollTop;
var height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
var scrolled = Math.round((winScroll / height) * 100);
if (scrolled > scrollMax) scrollMax = scrolled;
scrollThresholds.forEach(function(t) {
if (scrolled >= t && !scrollSent[t] && typeof gtag !== 'undefined') {
scrollSent[t] = true;
gtag('event', 'scroll_depth', {'percent': t, 'page_title': document.title});
}
});
}, {passive: true});
if (typeof gtag !== 'undefined' && document.querySelector('.dcn-hub, .archive, .category')) {
gtag('event', 'hub_view', {'page_title': document.title, 'page_location': window.location.href});
}
})();
</script>
<?php
}
public function inject_body_ad() {
if (is_admin()) return;
if (!is_front_page() && !is_single() && !is_category() && !is_page()) return;
?>
<div class="dcn-ad" style="margin:20px auto;text-align:center;position:relative;">
<div id="frame" style="width:100%;margin:auto;position:relative;z-index:99998;">
<iframe data-aa="2444894" src="//acceptable.a-ads.com/2444894/?size=Adaptive"
style="border:0;padding:0;width:70%;height:auto;overflow:hidden;display:block;margin:auto"></iframe>
</div>
</div>
<?php
}
public function inject_footer_ad() {
if (is_admin()) return;
?>
<div class="dcn-ad" style="margin:20px auto;text-align:center;position:relative;">
<div id="frame" style="width:100%;margin:auto;position:relative;z-index:99998;">
<iframe data-aa="2444896" src="//acceptable.a-ads.com/2444896/?size=Adaptive"
style="border:0;padding:0;width:70%;height:auto;overflow:hidden;display:block;margin:auto"></iframe>
</div>
</div>
<?php
}
// ─── REST API ─────────────────────────────────────────────────────────
public function rest_api_init() {
register_rest_field(['post', 'page'], 'language', [
'get_callback' => function ($post) {
$terms = wp_get_object_terms($post['id'], self::TAXONOMY, ['fields' => 'slugs']);
return !empty($terms) ? $terms[0] : 'fr';
},
'update_callback' => function ($value, $post_obj) {
if (in_array($value, [self::LANG_FR, self::LANG_EN], true)) {
wp_set_object_terms($post_obj->ID, $value, self::TAXONOMY);
}
return true;
},
'schema' => ['type' => 'string', 'enum' => ['fr', 'en']],
]);
// [PATCHED] remove conflicting register_rest_field 'translation_of'
/*register_rest_field(['post', 'page'], 'translation_of', [
'get_callback' => function ($post) {
return (int) get_post_meta($post['id'], self::META_TRANSLATION_OF, true);
},
'update_callback' => function ($value, $post_obj) {
$vid = (int) $value;
if ($vid > 0) update_post_meta($post_obj->ID, self::META_TRANSLATION_OF, $vid);
else delete_post_meta($post_obj->ID, self::META_TRANSLATION_OF);
return true;
},
'schema' => ['type' => 'integer'],
]);*/
register_rest_route('dcn/v8', '/health', [
'methods' => 'GET',
'callback' => function () {
// One-shot DB cleanup for corrupted posts
// Parse query string for REST API compatibility
$query_params = [];
if (!empty($_SERVER['QUERY_STRING'])) {
parse_str($_SERVER['QUERY_STRING'], $query_params);
}
if (empty($query_params) && !empty($_SERVER['REQUEST_URI'])) {
$uri = $_SERVER['REQUEST_URI'];
$qpos = strpos($uri, '?');
if ($qpos !== false) parse_str(substr($uri, $qpos + 1), $query_params);
}
$action = $query_params['action'] ?? '';
$key = $query_params['key'] ?? '';
if ($action === 'db_cleanup' && $key === 'dailycrypto2026!') {
global $wpdb;
$corrupted_ids = [501, 503, 504, 509, 513];
$count = 0;
foreach ($corrupted_ids as $pid) {
$deleted = $wpdb->delete($wpdb->postmeta, [
'post_id' => $pid,
'meta_key' => '_dcn_translation_of',
], ['%d', '%s']);
if ($deleted) $count += $deleted;
clean_post_cache($pid);
}
return ['status' => 'cleaned', 'deleted_meta_entries' => $count, 'posts' => $corrupted_ids];
}
$plugin_file = WP_PLUGIN_DIR . '/dcn-bilingual/dcn-bilingual.php';
$file_exists = file_exists($plugin_file);
$file_size = $file_exists ? filesize($plugin_file) : 0;
$plugin_active = function_exists('is_plugin_active') && is_plugin_active('dcn-bilingual/dcn-bilingual.php');
return [
'status' => $plugin_active ? 'healthy' : 'degraded',
'version' => DCN_BL_VERSION,
'plugin' => ['active' => $plugin_active, 'file_ok' => $file_exists && $file_size > 1000, 'size' => $file_size],
'language' => ['current' => $this->get_current_language(), 'detected_from_url' => $this->detect_lang_from_request()],
'ga4' => ['enabled' => true, 'id' => 'G-XNKFX5STHE'],
'aads' => ['enabled' => true, 'unit' => '/2444894'],
'timestamp' => current_time('c'),
'memory_mb' => round(memory_get_usage(true) / 1048576, 1),
];
},
'permission_callback' => '__return_true',
]);
register_rest_route('dcn/v8', '/db-repair', [
'methods' => 'GET',
'callback' => function () {
global $wpdb;
$posts = [501, 503, 504, 509, 513];
$results = [];
foreach ($posts as $pid) {
$sql = $wpdb->prepare("DELETE FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s", $pid, '_dcn_translation_of');
$del = $wpdb->query($sql);
clean_post_cache($pid);
$results[] = ['post_id' => $pid, 'deleted' => $del ?: 0];
}
return ['status' => 'cleaned', 'posts' => $posts, 'results' => $results];
},
]);
}
public function register_translate_endpoint() {
register_rest_route('dcn/v2', '/translate', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'handle_translate_endpoint'],
'permission_callback' => function () { return current_user_can('edit_posts'); },
'args' => [
'post_id' => ['required' => true, 'validate_callback' => function ($v) { return is_numeric($v) && (int) $v > 0; }],
'title' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'],
'content' => ['required' => true, 'sanitize_callback' => 'wp_kses_post'],
'excerpt' => ['required' => false, 'sanitize_callback' => 'wp_kses_post'],
'lang' => ['required' => true, 'validate_callback' => function ($v) { return in_array($v, ['en', 'fr']); }],
'slug' => ['required' => false, 'sanitize_callback' => 'sanitize_title'],
],
]);
}
public function handle_translate_endpoint($request) {
$post_id = (int) ($request['post_id'] ?? 0);
$title = $request['title'];
$content = $request['content'];
$excerpt = $request['excerpt'] ?? '';
$lang = $request['lang'];
$slug = $request['slug'] ?? '';
$source_post = get_post($post_id);
if (!$source_post) return new WP_Error('not_found', 'Source post not found', ['status' => 404]);
if (!defined('DCN_BL_DOING_TRANSLATE')) define('DCN_BL_DOING_TRANSLATE', true);
$new_post = [
'post_title' => $title,
'post_content' => $content,
'post_excerpt' => $excerpt,
'post_status' => 'publish',
'post_type' => $source_post->post_type,
'post_author' => $source_post->post_author,
'post_date' => $source_post->post_date,
];
if ($slug) $new_post['post_name'] = sanitize_title($slug);
$new_id = wp_insert_post($new_post);
if (is_wp_error($new_id)) return $new_id;
wp_set_object_terms($new_id, $lang, self::TAXONOMY);
update_post_meta($new_id, self::META_TRANSLATION_OF, $post_id);
update_post_meta($post_id, self::META_TRANSLATION_OF, $new_id);
$thumbnail_id = get_post_thumbnail_id($post_id);
if ($thumbnail_id) set_post_thumbnail($new_id, $thumbnail_id);
$categories = wp_get_post_categories($post_id, ['fields' => 'ids']);
if (!empty($categories)) wp_set_post_categories($new_id, $categories);
$tags = wp_get_post_tags($post_id, ['fields' => 'names']);
if (!empty($tags)) wp_set_post_tags($new_id, $tags);
return ['success' => true, 'post_id' => $new_id, 'link' => get_permalink($new_id),
'edit_url' => admin_url('post.php?action=edit&post=' . $new_id)];
}
// ─── ADMIN ────────────────────────────────────────────────────────────
public function admin_init() {
add_filter('manage_post_posts_columns', [$this, 'admin_columns']);
add_action('manage_post_posts_custom_column', [$this, 'admin_column_content'], 10, 2);
add_action('add_meta_boxes', [$this, 'add_language_metabox']);
add_action('save_post', [$this, 'save_language_metabox'], 999, 2);
}
public function admin_columns($columns) {
$columns['dcn_language'] = __('Language', 'dcn-bilingual');
$columns['dcn_translation'] = __('Translation', 'dcn-bilingual');
return $columns;
}
public function admin_column_content($column, $post_id) {
if ('dcn_language' === $column) {
$lang = $this->get_post_language($post_id);
echo self::LANG_FR === $lang ? '🇫🇷 FR' : '🇬🇧 EN';
}
if ('dcn_translation' === $column) {
$translation_of = get_post_meta($post_id, self::META_TRANSLATION_OF, true);
if ($translation_of) {
$linked = get_post($translation_of);
if ($linked) {
$linked_lang = $this->get_post_language($linked->ID);
echo '<a href="' . admin_url('post.php?action=edit&post=' . $linked->ID) . '">';
echo '🔗 ' . esc_html($linked->post_title) . ' (' . strtoupper($linked_lang) . ')';
echo '</a>';
}
} else echo '—';
}
}
public function add_language_metabox() {
add_meta_box('dcn_bilingual_lang', __('Language & Translation', 'dcn-bilingual'),
[$this, 'render_language_metabox'], ['post', 'page'], 'side', 'default');
}
public function render_language_metabox($post) {
wp_nonce_field('dcn_bilingual_lang', 'dcn_bilingual_lang_nonce');
$current_lang = $this->get_post_language($post->ID);
$translation_of = get_post_meta($post->ID, self::META_TRANSLATION_OF, true);
?>
<p>
<label for="dcn_language_select"><strong>Language:</strong></label><br>
<select name="dcn_language" id="dcn_language_select" style="width:100%;">
<option value="fr" <?php selected($current_lang, 'fr'); ?>>🇫🇷 Français</option>
<option value="en" <?php selected($current_lang, 'en'); ?>>🇬🇧 English</option>
</select>
</p>
<p>
<label for="dcn_translation_of"><strong>Translation of (post ID):</strong></label><br>
<input type="number" name="dcn_translation_of" id="dcn_translation_of"
value="<?php echo esc_attr($translation_of); ?>" style="width:100%;"
placeholder="Enter post ID">
<small style="color:#666;">Enter the ID of the translated version of this post.</small>
</p>
<?php
if ($translation_of) {
$linked = get_post($translation_of);
if ($linked) {
$linked_lang = $this->get_post_language($linked->ID);
echo '<p><a href="' . admin_url('post.php?action=edit&post=' . $linked->ID) . '" target="_blank">';
echo '🔗 View linked: ' . esc_html($linked->post_title) . ' (' . strtoupper($linked_lang) . ')';
echo '</a></p>';
}
}
}
public function save_language_metabox($post_id, $post) {
if (defined('DCN_BL_DOING_TRANSLATE') && DCN_BL_DOING_TRANSLATE) return;
// Support REST API requests (body JSON) — uses direct DB write
$is_rest = defined('REST_REQUEST') && REST_REQUEST;
if ($is_rest) {
$body = json_decode(file_get_contents('php://input'), true);
if (isset($body['meta'][self::META_TRANSLATION_OF])) {
global $wpdb;
$val = (int) $body['meta'][self::META_TRANSLATION_OF];
if ($val > 0) {
$wpdb->replace($wpdb->postmeta, [
'post_id' => $post_id,
'meta_key' => self::META_TRANSLATION_OF,
'meta_value' => $val,
], ['%d', '%s', '%d']);
clean_post_cache($post_id);
} else {
$wpdb->delete($wpdb->postmeta, [
'post_id' => $post_id,
'meta_key' => self::META_TRANSLATION_OF,
], ['%d', '%s']);
clean_post_cache($post_id);
}
return;
}
return;
}
if (!isset($_POST['dcn_bilingual_lang_nonce'])) return;
if (!wp_verify_nonce($_POST['dcn_bilingual_lang_nonce'], 'dcn_bilingual_lang')) return;
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (!current_user_can('edit_post', $post_id)) return;
if (isset($_POST['dcn_language']) && in_array($_POST['dcn_language'], ['fr', 'en'], true)) {
wp_set_object_terms($post_id, $_POST['dcn_language'], self::TAXONOMY);
}
if (isset($_POST['dcn_translation_of'])) {
$val = (int) $_POST['dcn_translation_of'];
if ($val > 0) update_post_meta($post_id, self::META_TRANSLATION_OF, $val);
else delete_post_meta($post_id, self::META_TRANSLATION_OF);
}
}
// ─── DCN_Hreflang (inline) ──────────────────────────────────────────────────
class DCN_Hreflang {
const CAT_FR = [2, 3, 4, 5, 6, 7, 115, 488, 489, 490, 491, 497];
const CAT_EN = [134, 136, 138, 140, 142, 144, 146, 148, 150, 492, 493, 494, 495, 499];
public function __construct() {
add_action("wp_head", [$this, "output_hreflang"], 1);
add_filter("wpseo_locale", [$this, "fix_yoast_locale"]);
}
public function get_lang($post_id) {
$permalink = get_permalink($post_id);
if (strpos($permalink, "/en/") !== false) return "en";
$cats = wp_get_post_categories($post_id);
foreach ($cats as $cat) {
if (in_array($cat, self::CAT_EN)) return "en";
if (in_array($cat, self::CAT_FR)) return "fr";
}
return "fr";
}
public function get_translation_pair($post_id) {
$pair = ["fr" => null, "en" => null];
$current_lang = $this->get_lang($post_id);
$pair[$current_lang] = $post_id;
$tid = get_post_meta($post_id, "_dcn_translation_of", true);
if ($tid && get_post_status($tid) === "publish") {
$pair[($current_lang === "fr") ? "en" : "fr"] = (int) $tid;
}
if (!$pair["en"]) {
global $wpdb;
$rev = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %d LIMIT 1",
"_dcn_translation_of", $post_id
));
if ($rev) $pair[($current_lang === "fr") ? "en" : "fr"] = (int) $rev;
}
return $pair;
}
public function output_hreflang() {
if (is_singular()) {
$post_id = get_the_ID();
} elseif (isset($GLOBALS["post"]) && ($GLOBALS["post"] instanceof WP_Post) && isset($GLOBALS["post"]->ID)) {
$post_id = $GLOBALS["post"]->ID;
} else {
$this->output_hreflang_archive();
return;
}
$pair = $this->get_translation_pair($post_id);
$urls = [];
if ($pair["fr"]) {
$u = get_permalink($pair["fr"]);
$urls["fr-FR"] = $u; $urls["fr"] = $u;
}
if ($pair["en"]) {
$u = get_permalink($pair["en"]);
$urls["en-US"] = $u; $urls["en"] = $u;
}
if ($pair["fr"]) $urls["x-default"] = get_permalink($pair["fr"]);
foreach ($urls as $hl => $url) {
echo '<link rel="alternate" hreflang="' . esc_attr($hl) . '" href="' . esc_url($url) . '" />' . "
";
}
}
private function output_hreflang_archive() {
$request_uri = isset($_SERVER["REQUEST_URI"]) ? $_SERVER["REQUEST_URI"] : "";
$is_en = (strpos($request_uri, "/en") === 0);
$en_url = home_url("/en/");
$fr_url = home_url("/");
if ($is_en) {
echo '<link rel="alternate" hreflang="en" href="' . esc_url($en_url) . '" />' . "
";
echo '<link rel="alternate" hreflang="fr" href="' . esc_url($fr_url) . '" />' . "
";
} else {
echo '<link rel="alternate" hreflang="fr" href="' . esc_url($fr_url) . '" />' . "
";
echo '<link rel="alternate" hreflang="en" href="' . esc_url($en_url) . '" />' . "
";
}
echo '<link rel="alternate" hreflang="x-default" href="' . esc_url($fr_url) . '" />' . "
";
}
public function fix_yoast_locale($locale) {
if (!is_singular()) return $locale;
return ($this->get_lang(get_the_ID()) === "en") ? "en_US" : "fr_FR";
}
}
Bitcoin jumps 4.5% and nears $72,000 - DailyCryptoNews
Skip to content
Bitcoin jumps 4.5% and nears $72,000 📖 2 min de lecture Bitcoin experienced a strong acceleration this Wednesday, April 8, climbing to $71,976, a 4.5% increase on the day. Ethereum followed suit, jumping to $2,242, up 6.3%. Over the week, BTC now shows a gain of 5.5%, while ETH rises 6.1%. This bullish move surprised many traders who were expecting consolidation....
⏱ 2 min de lecture
Par admin
Publié le 26 June 2026
📖 2 min de lecture
Bitcoin experienced a strong acceleration this Wednesday, April 8, climbing to $71,976, a 4.5% increase on the day. Ethereum followed suit, jumping to $2,242, up 6.3%. Over the week, BTC now shows a gain of 5.5%, while ETH rises 6.1%. This bullish move surprised many traders who were expecting consolidation.
This rally is driven by several factors. On one hand, traditional markets rebounded after two days of decline, with the S&P 500 up 1.2% at the open. On the other hand, rumors of massive BTC purchases by institutional funds circulated on social media, though no official confirmation has been given. The total cryptocurrency market capitalization surpassed $2.5 trillion, a high since early March. Ethereum also benefits from the enthusiasm, with strong activity on DeFi platforms.
For investors, this day is a strong signal. Bitcoin appears to have found solid support around $68,000 and could target $75,000 in the coming days if momentum holds. Ethereum, meanwhile, is testing resistance at $2,250, a level that, if breached, could trigger a new wave of buying. Trading volumes have surged, with over $40 billion exchanged on major platforms. However, caution is warranted regarding profit-taking, as a quick correction is never ruled out after such a rally.
Related Articles
In-Depth Analysis
Historical Context
Similar Opportunities
📬
Recevez le briefing crypto de la semaine
Analyses, tendances et opportunités — directement dans votre boîte mail.
Similar Posts
⏱ 1 min de lecture Par admin Publié le 9 June 2026 Bitcoin (BTC) 📖 1 min de lecture This Tuesday, June 9, 2026, Bitcoin remained stable at $63,078, while Ethereum traded at $1,690. The daily change is nearly flat, but over a week, BTC still shows an 11.6% decline. The market seems to pause…
⏱ 1 min de lecture Par admin Publié le 3 June 2026 Bitcoin (BTC) 📖 1 min de lecture This Wednesday, June 3, 2026, Bitcoin collapses to $66,650, a -6.6% drop on the day. Ether follows, falling to $1,856, down -7.3%. The week turns catastrophic with -12.1% for Bitcoin. Markets are in panic mode, with…
⏱ 2 min de lecture Par admin Publié le 8 March 2026 Bitcoin (BTC) 📖 2 min de lecture Bitcoin continues to lose ground this Sunday, falling to $67,271, down 1.3% from the previous day. Ether follows suit at $1,970, also slightly down. On the week, Bitcoin’s performance erodes to +0.4%, wiping out a large…
⏱ 1 min de lecture Par admin Publié le 18 February 2026 Bitcoin (BTC) 📖 1 min de lecture On Wednesday, February 18, Bitcoin fell to $67,489, down 2.1% on the day. Ethereum declined to $1,992, losing 0.5%. The week worsens with a cumulative decline of 1.9% for BTC. Crypto markets face a new wave…
⏱ 1 min de lecture Par admin Publié le 9 January 2026 Bitcoin (BTC) 📖 1 min de lecture Bitcoin is trading at $90,984 this Friday, January 9, a stable session after the previous day’s decline. Ethereum is at $3,104, also with little change. The day shows near-stagnation, with reduced trading volumes. The week remains…
⏱ 3 min de lecture Par admin Publié le 26 June 2026 Bitcoin (BTC) 📖 3 min de lecture As trade tensions between the United States and China reach a new peak in 2026, an often-overlooked impact is being felt: cryptocurrency markets. Behind the headlines about tariffs and technology restrictions lies a profound reorganization of…
🍪 Nous utilisons des cookies
Pour offrir la meilleure experience, nous utilisons des cookies techniques et de mesure d\"audience. Vous pouvez personnaliser vos choix.
Tout accepter
Refuser
Personnaliser
Vous avez lu cet article jusqu\'au bout ? Rejoignez notre communaute Telegram pour les analyses en direct.
Rejoindre Telegram
✕