load_dependencies(); $this->loader = new ABDS_SEO_Loader(); $this->define_core_hooks(); $this->define_migration_hooks(); $this->define_scanner_hooks(); $this->define_audit_hooks(); $this->define_content_score_cache_hooks(); $this->define_history_hooks(); $this->define_api_hooks(); $this->define_frontend_hooks(); $this->define_sitemap_hooks(); $this->define_indexnow_hooks(); $this->define_robots_editor_hooks(); // Deferred to 'init' (unlike every other define_*_hooks() call here, // which run directly): as of 2026-09-02 this method resolves real // __() translations while building $canary_translations, and // ABDS_SEO_Core's own constructor runs before 'plugins_loaded' even // fires -- calling __() that early trips WordPress's own "translation // loading triggered too early" _doing_it_wrong() notice (added 6.7), // confirmed live. 'init' is WordPress's own stated minimum safe point. $this->loader->add_action( 'init', $this, 'define_translation_canary_hooks' ); $this->define_admin_hooks(); } /** * Requires every class/interface/trait file the plugin needs. */ protected function load_dependencies(): void { require_once ABDS_SEO_PLUGIN_DIR . 'includes/migration/interface-abds-seo-migration-adapter.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/migration/trait-abds-seo-plugin-file-locator.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/migration/class-abds-seo-adapter-yoast.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/migration/class-abds-seo-adapter-rankmath.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/migration/class-abds-seo-adapter-aioseo.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/migration/class-abds-seo-migration-manager.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/scanner/class-abds-seo-site-scanner.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/scanner/class-abds-seo-schema-validator.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/scanner/class-abds-seo-scan-scheduler.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-keyword-extractor.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-readability-checker.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-content-ref.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-block-content-adapter.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-content-boundary-detector.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-content-boundary-fetcher.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-rendered-html-content-adapter.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-rules-engine.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-block-recommendations.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-content-analyzer.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-secondary-keywords.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/content-analysis/class-abds-seo-content-score-cache.php'; // R3 (Audit 2.0): a normalized-per-issue background audit, independent // of the scanner/content-analysis pair above (which it wraps as checks, // see each adapter's own docblock) — requires those two blocks loaded first. require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/interface-abds-seo-audit-check.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-audit-check-technical.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-audit-check-content.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-audit-check-schema.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-audit-engine.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-audit-scheduler.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-issue-catalog.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-audit-frontend-overlay.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/audit/class-abds-seo-audit-highlight-overlay.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/history/class-abds-seo-changes-log.php'; // Shared secret-at-rest primitive (AES-256-GCM over wp_salt). Used by the // free licence store and by several Pro modules' key vaults, so it lives // in the free plugin and stays here when the Pro modules move out. require_once ABDS_SEO_PLUGIN_DIR . 'includes/crypto/class-abds-seo-ai-crypto.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/priority/class-abds-seo-priority-scorer.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/priority/class-abds-seo-priority-service.php'; // LocalBusiness JSON-LD validator stays in the free plugin: the free // scanner + frontend meta output both reuse it for schema validation and // LocalBusiness output. The NAP scanner itself is the Pro Local SEO // feature and moves to the add-on. require_once ABDS_SEO_PLUGIN_DIR . 'includes/local/class-abds-seo-localbusiness-validator.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/frontend/class-abds-seo-template-resolver.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/frontend/class-abds-seo-meta-builder.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/frontend/class-abds-seo-schema-graph.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/frontend/class-abds-seo-schema-context.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/frontend/class-abds-seo-schema-settings.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/frontend/class-abds-seo-compat-detector.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/frontend/class-abds-seo-frontend-output.php'; // R1 Policy Engine v1: a new read-only resolve(url|object) service // (see class docblock). Loaded so it's available, but nothing in // R1 hooks into or calls it yet — real consumers land in R2-R4. require_once ABDS_SEO_PLUGIN_DIR . 'includes/policy/class-abds-seo-policy-engine.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/policy/class-abds-seo-noindex-count.php'; // R2 shared foundations: public post-type/taxonomy enumeration and the // versioned abds_seo_settings backfill (see that class's own docblock). require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-taxonomy-helper.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-settings-installer.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-author-count.php'; // F2.5's conscious-activation gate for the noindex/nofollow "wake up" // (see that class's own docblock) — the notice class registers an // admin_post handler too, so it's required unconditionally, same as // ABDS_SEO_License_Notices below. require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-noindex-activation.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-noindex-activation-notice.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/sitemap/class-abds-seo-sitemap-generator.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/sitemap/class-abds-seo-sitemap-coverage.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/sitemap/class-abds-seo-sitemap-controller.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/indexnow/class-abds-seo-indexnow-key.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/indexnow/class-abds-seo-indexnow-submitter.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-robots-txt-checker.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-robots-editor.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-rewrite-flush.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-site-identity.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-status-names.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-response.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-store.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-client.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-scheduler.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-controller.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-grace.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-notices.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-feature-map.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-feature-presentation.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-gate.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-license-accessor.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/license/class-abds-seo-pricing.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/reports/class-abds-seo-reports.php'; // Agency cabinet moved to the ABDS SEO Pro add-on (SEO-C/C1). The free // core no longer ships or wires it; the add-on registers it on its own // loader when the site holds an Agency licence. require_once ABDS_SEO_PLUGIN_DIR . 'includes/onboarding/class-abds-seo-onboarding.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-ui-preferences.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-translation-canary.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/api/class-abds-seo-rest-base.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/api/class-abds-seo-rest-controller.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/api/class-abds-seo-search-appearance-rest-controller.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/api/class-abds-seo-indexing-rest-controller.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/api/class-abds-seo-audit-rest-controller.php'; // Redirects + 404 log (0.15.0). require_once ABDS_SEO_PLUGIN_DIR . 'includes/redirects/class-abds-seo-redirect-matcher.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/redirects/class-abds-seo-not-found-policy.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/redirects/class-abds-seo-redirect-repository.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/redirects/class-abds-seo-not-found-log.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/redirects/class-abds-seo-redirect-suggestions.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/redirects/class-abds-seo-redirect-runtime.php'; require_once ABDS_SEO_PLUGIN_DIR . 'includes/api/class-abds-seo-redirects-rest-controller.php'; if ( is_admin() ) { require_once ABDS_SEO_PLUGIN_DIR . 'admin/class-abds-seo-admin.php'; require_once ABDS_SEO_PLUGIN_DIR . 'admin/class-abds-seo-score-visibility.php'; } if ( defined( 'WP_CLI' ) && WP_CLI ) { require_once ABDS_SEO_PLUGIN_DIR . 'includes/class-abds-seo-cli-commands.php'; } } /** * Queues i18n, DB-upgrade, and post-meta-registration hooks. */ protected function define_core_hooks(): void { // No load_plugin_textdomain(): WordPress.org auto-loads the plugin's PHP // translations by slug since WP 4.6. We only redirect the JS catalogue // lookup below (our combined install-path-independent bundle). $i18n = new ABDS_SEO_I18n(); $this->loader->add_filter( 'load_script_translation_file', $i18n, 'filter_script_translation_file', 10, 3 ); $this->loader->add_action( 'plugins_loaded', 'ABDS_SEO_DB_Installer', 'maybe_upgrade' ); $this->loader->add_action( 'plugins_loaded', 'ABDS_SEO_Activator', 'maybe_upgrade_caps' ); $this->loader->add_action( 'plugins_loaded', 'ABDS_SEO_Settings_Installer', 'maybe_upgrade' ); $this->loader->add_action( 'plugins_loaded', 'ABDS_SEO_Noindex_Activation', 'maybe_flag_pending_activation' ); $this->loader->add_action( 'admin_init', 'ABDS_SEO_Rewrite_Flush', 'maybe_flush' ); $this->loader->add_action( 'init', $this, 'register_meta_fields' ); } /** * Registers the post meta fields the Gutenberg sidebar reads/writes via * the core editor's meta REST fields (show_in_rest), so no bespoke * REST route is needed just to persist snippet/focus-keyword edits. */ public function register_meta_fields(): void { $fields = array( '_abds_seo_title' => 'string', '_abds_seo_description' => 'string', '_abds_seo_focus_keyword' => 'string', '_abds_seo_canonical' => 'string', ); foreach ( get_post_types( array( 'public' => true ) ) as $post_type ) { foreach ( $fields as $meta_key => $type ) { register_post_meta( $post_type, $meta_key, array( 'type' => $type, 'single' => true, 'show_in_rest' => true, 'auth_callback' => fn( $allowed, $key, $post_id ) => current_user_can( 'edit_post', $post_id ), 'sanitize_callback' => 'sanitize_text_field', ) ); } // 3.3 (multi-keyword): deliberately a SEPARATE field from // _abds_seo_focus_keyword, not a migration of it into an array. // _abds_seo_focus_keyword stays the single source of the primary // keyword — every existing reader (8 in free, 6 in Pro, including // Pro's raw-SQL covered_keywords()) keeps working unchanged. This // field holds only the SECONDARY keywords, Pro-only, ordered // (index 0 = highest priority among the secondaries), capped at // ABDS_SEO_Content_Analyzer::MAX_SECONDARY_KEYWORDS server-side — // the REST schema itself can't enforce a max array length. register_post_meta( $post_type, '_abds_seo_secondary_keywords', array( 'type' => 'array', 'single' => true, 'default' => array(), 'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ), ), ), 'auth_callback' => fn( $allowed, $key, $post_id ) => current_user_can( 'edit_post', $post_id ), 'sanitize_callback' => array( 'ABDS_SEO_Secondary_Keywords', 'sanitize' ), ) ); } } /** * Migration has no hooks of its own — it's triggered from the admin UI * / REST API, not on every request (see ABDS_SEO_Admin and * ABDS_SEO_Rest_Controller). Kept as an explicit no-op for symmetry * with the other define_*_hooks() methods. */ protected function define_migration_hooks(): void { } /** * Queues the scan scheduler's Action Scheduler hooks. */ protected function define_scanner_hooks(): void { $scheduler = new ABDS_SEO_Scan_Scheduler(); $scheduler->register_hooks( $this->loader ); } /** * Queues the R3 (Audit 2.0) background audit's Action Scheduler batch * hook — its own dedicated method (not folded into * define_scanner_hooks(), the pre-R3 scanner it's independent from) so * the two on-demand pipelines stay easy to tell apart at a glance. */ protected function define_audit_hooks(): void { $audit_scheduler = new ABDS_SEO_Audit_Scheduler(); $audit_scheduler->register_hooks( $this->loader ); $frontend_overlay = new ABDS_SEO_Audit_Frontend_Overlay( $audit_scheduler ); $frontend_overlay->register_hooks( $this->loader ); $highlight_overlay = new ABDS_SEO_Audit_Highlight_Overlay(); $highlight_overlay->register_hooks( $this->loader ); } /** * Queues the save_post -> Action Scheduler content_score caching hooks. */ protected function define_content_score_cache_hooks(): void { $score_cache = new ABDS_SEO_Content_Score_Cache(); $score_cache->register_hooks( $this->loader ); $priority = new ABDS_SEO_Priority_Service(); $priority->register_hooks( $this->loader ); // Self-heals a site stuck in the pre-ABDS_Site_Identity broken state // as of this plugin's very next boot after updating -- not whenever // the daily license-check cron next happens to fire, which could be // most of a day away. Gated by an autoloaded version flag (same // idiom as CAPS_VERSION_OPTION/maybe_upgrade_caps() elsewhere in // this codebase), NOT run unconditionally on every boot: the // earlier reasoning here ("a single get_option() that returns // immediately" is cheap) missed that ABDS_Site_Identity::OPTION is // deliberately non-autoloaded, so that "single get_option()" was a // real extra DB query on every request -- including anonymous // frontend hits, which readme.txt promises this plugin never slows // down. See ABDS_SEO_License_Store::maybe_heal_instance_hash(). $this->loader->add_action( 'plugins_loaded', 'ABDS_SEO_License_Store', 'maybe_heal_instance_hash' ); $license_scheduler = new ABDS_SEO_License_Scheduler(); $license_scheduler->register_hooks( $this->loader ); $license_controller = new ABDS_SEO_License_Controller(); $license_controller->register_hooks( $this->loader ); $license_notices = new ABDS_SEO_License_Notices(); $license_notices->register_hooks( $this->loader ); $noindex_activation_notice = new ABDS_SEO_Noindex_Activation_Notice(); $noindex_activation_notice->register_hooks( $this->loader ); // Self-hosted plugin updates live in the ABDS SEO Pro add-on (delivered // off WordPress.org). The free plugin updates through WordPress.org like // any hosted plugin, so it ships no updater — WP.org forbids them. // Agency cabinet (background push + its settings REST) now lives in the // ABDS SEO Pro add-on, which registers it on its own loader (SEO-C/C1). } /** * Queues the changes-log's meta-update hooks (SEO title/description/ * focus keyword only — see ABDS_SEO_Changes_Log's docblock for why * Gutenberg block content isn't tracked here). */ protected function define_history_hooks(): void { $changes_log = new ABDS_SEO_Changes_Log(); $changes_log->register_hooks( $this->loader ); } /** * Queues the REST API controller's routes. */ protected function define_api_hooks(): void { $rest_controller = new ABDS_SEO_Rest_Controller(); $rest_controller->register_hooks( $this->loader ); $search_appearance_rest = new ABDS_SEO_Search_Appearance_Rest_Controller(); $search_appearance_rest->register_hooks( $this->loader ); $indexing_rest = new ABDS_SEO_Indexing_Rest_Controller(); $indexing_rest->register_hooks( $this->loader ); $audit_rest = new ABDS_SEO_Audit_Rest_Controller(); $audit_rest->register_hooks( $this->loader ); $redirects_rest = new ABDS_SEO_Redirects_Rest_Controller(); $redirects_rest->register_hooks( $this->loader ); ( new ABDS_SEO_Redirect_Runtime() )->register_hooks( $this->loader ); ( new ABDS_SEO_Redirect_Suggestions() )->register_hooks( $this->loader ); $onboarding = new ABDS_SEO_Onboarding(); $onboarding->register_hooks( $this->loader ); $ui_preferences = new ABDS_SEO_UI_Preferences(); $ui_preferences->register_hooks( $this->loader ); } /** * Queues the frontend SEO-tag output hooks (title, description, * canonical, OG, Twitter, JSON-LD), skipped in wp-admin since they * only fire on front-end requests anyway. Compatibility mode is * re-checked inside the hooks on every request, not here. */ protected function define_frontend_hooks(): void { if ( is_admin() ) { return; } $frontend = new ABDS_SEO_Frontend_Output(); $frontend->register_hooks( $this->loader ); // P3 (2026-09-02 backlog): the_content marker used only by this // plugin's own content-boundary loopback probe (see that class) — // gated the same way as $frontend above, since the marker only // matters on the real front-end permalink request the probe makes, // never on an admin request. $boundary_fetcher = new ABDS_SEO_Content_Boundary_Fetcher(); $boundary_fetcher->register_hooks( $this->loader ); } /** * Queues the R2 (F2.6) sitemap controller's hooks — unconditionally, * unlike define_frontend_hooks(): add_rewrite_rule() must register the * rule on every 'init' (admin requests included), since a * flush_rewrite_rules() call can itself originate from wp-admin (e.g. * a Settings save) and needs the full rule set present at that moment, * not just on the next front-end hit. */ protected function define_sitemap_hooks(): void { $sitemap = new ABDS_SEO_Sitemap_Controller(); $sitemap->register_hooks( $this->loader ); } /** * Queues M7 free (IndexNow)'s two independent concerns: the key file * (unconditional, same "add_rewrite_rule() every init" reasoning as * define_sitemap_hooks() above) and the submit-on-publish trigger * (gated on the settings toggle inside its own maybe_schedule(), not * here — see ABDS_SEO_Indexnow_Submitter's own docblock). */ protected function define_indexnow_hooks(): void { $key = new ABDS_SEO_Indexnow_Key(); $key->register_hooks( $this->loader ); $submitter = new ABDS_SEO_Indexnow_Submitter(); $submitter->register_hooks( $this->loader ); } /** * Queues the robots.txt editor's `robots_txt` filter — unconditionally, * same reasoning as define_sitemap_hooks(): a robots.txt request can * arrive on the front end at any time, not just from wp-admin. */ protected function define_robots_editor_hooks(): void { $robots_editor = new ABDS_SEO_Robots_Editor(); $robots_editor->register_hooks( $this->loader ); } /** * This plugin's own eight canary strings for ABDS_Translation_Canary * (SEO-TZ 1.5), rebuilt 2026-09-03 to the current selection rule — * see ABDS_Translation_Canary::$canary_translations for the rule and * why it requires a full sentence (roughly six words or more) for * every entry now, not "at least half". The original list ("Settings", * "Reports", "SEO title", "Meta description", "Focus keyword", "Run * again", "What the score is made of") was mostly short terms and * menu labels; on a real site running this plugin alongside ABDS SEO * Pro, a genuine translation-loading bug left 5 of these 8 strings * truly untranslated, but the 3 short SEO terms ("SEO title", "Meta * description", "Focus keyword") still happened to translate anyway * because Pro's own, unrelated translation file separately contained * the same terms — so the canary's "none translated" threshold never * fired on a bug it was specifically built to catch. Every entry * below is a real sentence instead, each independently confirmed * (2026-09-03) to correctly go untranslated in that same reproduced * scenario. Spans several different source files so a narrow, * single-file issue isn't mistaken for total breakage; each is a * genuinely free-plugin-specific claim, not upsell/teaser copy that * describes what a paid add-on does (that copy is exactly the kind * of text a same-family add-on is likely to phrase similarly). * * Single source of truth for both the live check and * tests/TranslationCanaryPotTest.php, which confirms every entry here * is still a real msgid in the generated .pot — a string getting * rephrased without updating this list fails a test immediately * instead of silently degrading the check on real sites. */ const TRANSLATION_CANARY_STRINGS = array( 'Average of the technical and content scores across analyzed pages.', 'Average content score across analyzed pages.', 'Leave blank to use the Global title template.', 'Enter a focus keyword to see block-level recommendations.', 'Add the focus keyword to the page title.', 'Mention the keyword in the first 100 words of the text.', 'This page is under %d words — consider expanding it.', 'The title exceeds the recommended length (60 characters).', ); /** * Queues the shared translation-canary detector's hooks — unconditionally, * like the sitemap/robots-editor hooks above: admin_notices and * admin_post fire on real requests regardless of is_admin() timing * here, and upgrader_process_complete can originate from any context. * Registers its own hooks directly (not via $this->loader) since this * class is shared verbatim with ABDS Connect and can't depend on this * plugin's own Loader. * * ABDS_Translation_Canary translates nothing itself (fixed 2026-09-02 * — see that class's own docblock): every string below is a literal * __() call in THIS file, with this plugin's own literal 'abds-seo' * domain, so `wp i18n make-pot` can see and extract it. The eight * calls building $canary_translations are deliberately written out * one by one, matching self::TRANSLATION_CANARY_STRINGS exactly, * rather than looping over that constant — a loop's msgid argument * would be a variable, which make-pot cannot extract, defeating the * fix for the same reason a dynamic domain did. * * Public, and itself called via $this->loader on 'init' rather than * directly from the constructor like every other define_*_hooks() * method here — see the constructor's own comment at that call site * for why the __() calls below need that deferral. */ public function define_translation_canary_hooks(): void { $canary_translations = array( 'Average of the technical and content scores across analyzed pages.' => __( 'Average of the technical and content scores across analyzed pages.', 'abds-seo' ), 'Average content score across analyzed pages.' => __( 'Average content score across analyzed pages.', 'abds-seo' ), 'Leave blank to use the Global title template.' => __( 'Leave blank to use the Global title template.', 'abds-seo' ), 'Enter a focus keyword to see block-level recommendations.' => __( 'Enter a focus keyword to see block-level recommendations.', 'abds-seo' ), 'Add the focus keyword to the page title.' => __( 'Add the focus keyword to the page title.', 'abds-seo' ), 'Mention the keyword in the first 100 words of the text.' => __( 'Mention the keyword in the first 100 words of the text.', 'abds-seo' ), /* translators: %d: word count -- this is only a canary lookup by exact msgid, never actually rendered with a real count. */ 'This page is under %d words — consider expanding it.' => __( 'This page is under %d words — consider expanding it.', 'abds-seo' ), 'The title exceeds the recommended length (60 characters).' => __( 'The title exceeds the recommended length (60 characters).', 'abds-seo' ), ); $canary = new ABDS_Translation_Canary( 'abds-seo', ABDS_SEO_VERSION, $canary_translations, array( 'notice_message' => __( "This plugin's translation for your site's language appears to be installed, but some screens are still showing in English. Try re-downloading translations (Dashboard → Updates → \"Update translations\") to see if that resolves it.", 'abds-seo' ), 'dismiss_label' => __( 'Got it, dismiss', 'abds-seo' ), 'permission_denied' => __( 'You do not have permission to perform this action.', 'abds-seo' ), ), ABDS_SEO_Activator::CAPABILITY ); $canary->register_hooks(); } /** * Queues the admin UI's hooks, only in wp-admin. */ protected function define_admin_hooks(): void { if ( ! is_admin() ) { return; } $admin = new ABDS_SEO_Admin(); $admin->register_hooks( $this->loader ); $score_visibility = new ABDS_SEO_Score_Visibility(); $score_visibility->register_hooks( $this->loader ); } /** * Registers every queued hook with WordPress. */ public function run(): void { $this->loader->run(); } }