WP Residence Help WP Residence Help

  • WPRESIDENCE
  • Video Tutorials
  • Client Support
  • API
Home / WPResidence Translate Plugin / Language Detection & Redirects — Developer Deep Dive

Language Detection & Redirects — Developer Deep Dive

44 views 0

This article is the code-level companion to the user-facing detection guide. It walks through how wpresidence-translate resolves the active language on every request, how it avoids canonical redirect loops, and which hooks it exposes for integration work on a multi-language real estate website.

Files & Entry Points

File Responsibility
includes/language-router.php Request path prep, language resolution, rewrite rules, cookie persistence, canonical-redirect guards.
includes/language-manager.php wpr_translate_resolve_preferred_language() – the cookie → browser → default fallback chain.
includes/language-manager-helpers.php wpr_translate_match_browser_language() – Accept-Language parser and matcher.
includes/body-language-attribute.php Prints the data-wpr-language attribute on <body>.
includes/seo-tags.php Emits canonical and hreflang link tags; integrates Yoast SEO and Rank Math.

Router Bootstrap

wpr_translate_language_router_bootstrap() registers the full hook set:

add_action( 'setup_theme', 'wpr_translate_language_router_prepare_request_path', 0 );
add_action( 'init',        'wpr_translate_language_router_prepare_request_path', 0 );
add_action( 'init',        'wpr_translate_language_router_register_rewrite_rules', 20 );
add_filter( 'query_vars',  'wpr_translate_language_router_register_query_vars' );
add_action( 'parse_request', 'wpr_translate_language_router_detect_language', 0 );
add_filter( 'redirect_canonical', 'wpr_translate_maybe_skip_front_page_canonical_redirect', 10, 2 );
add_filter( 'redirect_canonical', 'wpr_translate_maybe_skip_language_prefix_redirect',   2,  2 );
add_action( 'wp_ajax_wpr_translate_set_language_cookie',        'wpr_translate_handle_language_cookie_ajax' );
add_action( 'wp_ajax_nopriv_wpr_translate_set_language_cookie', 'wpr_translate_handle_language_cookie_ajax' );

Path preparation is bound to both setup_theme and init because some environments swap $_SERVER[‘REQUEST_URI’] between those hooks. A static $prepared flag inside the function ensures the work runs once per request.

Request Path Preparation

wpr_translate_language_router_prepare_request_path() is the first stage. It:

  1. Parses $_SERVER[‘REQUEST_URI’] into path and query pieces.
  2. Strips the home URL’s base-path segments (subdirectory installs).
  3. Compares the first remaining segment against every active language’s slug and code.
  4. On a match, stores the language in $GLOBALS[‘wpr_translate_language_router’][‘matched_language’], removes the prefix from the request URI, calls wpr_translate_set_current_language(), and rewrites $_SERVER[‘REQUEST_URI’] so the rest of WordPress sees the language-free path.

The function bails early when wpr_translate_language_router_should_skip_prepare() returns true – that covers admin, AJAX, CLI, and REST contexts. Both skip helpers are filterable via wpr_translate_language_router_skip_prepare and wpr_translate_language_router_skip_detection.

 

Query Vars & Rewrite Rules

Two public query vars are exposed:

  • wpr_lang – the language slug matched by the rewrite rule.
  • wpr_lang_path – the remainder of the path after the language prefix.

Rewrite rules are added at init priority 20, after all post types have registered. For each active language:

add_rewrite_tag( '%wpr_lang%',      '([^/]+)' );
add_rewrite_tag( '%wpr_lang_path%', '(.*)' );
add_rewrite_rule(
    '^' . preg_quote( $slug, '/' ) . '(?:/(.*))?/?$',
    'index.php?wpr_lang=' . $slug . '&wpr_lang_path=$matches[1]',
    'top'
);

Rules are inserted at the top of the list so they beat core archive rules. A rewrite flush is triggered from the activator; any runtime change to the language list must call flush_rewrite_rules() explicitly.

Language Detection on parse_request

wpr_translate_language_router_detect_language( $wp ) runs at priority 0 on parse_request and takes the following branches:

  1. If the path prep step already matched a language, use it.
  2. Else if the rewrite rule populated $wp->query_vars[‘wpr_lang’], resolve that via wpr_translate_get_language(), reconstruct the residual path from wpr_lang_path, and rewrite the request URI.
  3. Else call wpr_translate_get_default_language().
  4. As a final fallback, call wpr_translate_resolve_preferred_language(), which reads the cookie, then the browser header.

Whichever branch wins, wpr_translate_set_current_language() is called with reload_theme_translations => true, and WPR_TRANSLATE_CURRENT_LANGUAGE is defined as a convenience constant for templates.

The Cookie Persistence Contract

wpr_translate_language_router_store_language_cookie( $code ) writes the cookie named wpestate_translation_lang_pref with:

  • Expiration: time() + MONTH_IN_SECONDS.
  • Path: COOKIEPATH, mirrored to SITECOOKIEPATH when they differ.
  • Domain: COOKIE_DOMAIN.
  • Secure: is_ssl().
  • HttpOnly: true.

Critical invariant: the cookie is written only when language_from_url is true. Browser-header or default-language fallbacks never overwrite an existing preference – this prevents the cookie from being destroyed by background requests for missing asset files.

AJAX Endpoint for the Switcher

The front-end language switcher hits wp-admin/admin-ajax.php?action=wpr_translate_set_language_cookie. The handler:

  1. check_ajax_referer( ‘wpr_translate_language_cookie’, ‘nonce’ ).
  2. Sanitises the incoming language with sanitize_key().
  3. Confirms the language is registered and is_active.
  4. Calls wpr_translate_language_router_store_language_cookie().
  5. Responds with wp_send_json_success( [ ‘language’ => $code ] ).

Redirect-Loop Safeguards

Two redirect_canonical filters run on different priorities:

  • wpr_translate_maybe_skip_language_prefix_redirect (priority 2) – cancels redirects that only toggle a known language slug.
  • wpr_translate_maybe_skip_front_page_canonical_redirect (priority 10) – when the front page is a static page, the active language is non-default, and no translation of the front page exists, the filter returns false to keep the URL stable.

The front-page guard reads wpr_translation_get_translation_group() to confirm a translation does not exist before short-circuiting the redirect. If a translation is present, the canonical redirect is allowed through.

Browser Matching Algorithm

wpr_translate_match_browser_language() (in language-manager-helpers.php):

  1. Parses $_SERVER[‘HTTP_ACCEPT_LANGUAGE’], splits on ,, extracts ;q= quality.
  2. Normalises underscores to hyphens in each candidate.
  3. arsort() by quality.
  4. For each candidate, also tries its two-character root (so en-GB matches a language with code en).
  5. Matches against each language’s locale (hyphen-normalised), code, and slug.

SEO Tag Emission

wpr_translate_bootstrap_seo_tags() hooks wp_head at priority 2. wpr_translate_render_seo_link_tags() pulls URLs from wpr_translate_seo_collect_urls() and emits:

<link rel="alternate" hreflang="..." href="..." />
<link rel="canonical" href="..." />

When Yoast or Rank Math are active, the same URL set is handed to their filter hooks: wpseo_hreflang_links, wpseo_canonical, rank_math/frontend/seo/hreflang, rank_math/frontend/canonical.

Body Language Attribute

wpr_translate_print_body_language_attribute() hooks wp_body_open, wp_footer, and admin_footer. It emits a tiny inline script that calls document.body.setAttribute(‘data-wpr-language’, code). Static print-guard prevents double emission. Primarily useful for automated tests and analytics segmentation.

 

Settings Key

The wpr_translate_settings option carries the detect_browser_language boolean (default true, seeded by wpr_translate_activator_ensure_default_settings()). Code paths that consult it should read the option via get_option( ‘wpr_translate_settings’ ) rather than caching it.

Extension Points

  • apply_filters( ‘wpr_translate_language_router_skip_prepare’, $skip ) – force path prep to run in CLI tests.
  • apply_filters( ‘wpr_translate_language_router_skip_detection’, $skip ) – same for detection.
  • apply_filters( ‘wpr_translate_language_router_force_prepare’, false ) – bypass the static once-per-request guard.
  • $GLOBALS[‘wpr_translate_language_router’] – read-only view of the parsed state if you need the current relative path without the language prefix.

Gotchas

  • REST requests skip prep and detection. If you need language context in a REST handler, call wpr_translate_get_context_language() explicitly.
  • The cookie is HttpOnly – JavaScript cannot read it. Use the AJAX endpoint to change it, not direct DOM writes.
  • The two canonical filters sit at different priorities on purpose. Do not re-prioritise them without understanding the front-page case.

Related Reading

  • URL Structure & Permalinks – outbound URL construction.
  • Rewrite Rules & Query Vars – inbound request routing.
  • Translation Linking (trid system) – how translation groups power the front-page guard.

Product context is on the multi-language real estate website page.

WPResidence Translate Plugin

Related Articles

  • String Scanner — Developer Guide
  • The String Scanner
  • Gettext Pipeline & MO Files — Developer Guide
  • Gettext & MO Files — Making Translations Appear on the Front End

Help Categories

  • 18Agent, Agency & Developers
  • 5Blog Posts & Blog Lists
  • 38Elementor Shortcodes Built-In
  • 56FAQ
  • 15Footer
  • 5Getting Started
  • 37Header
  • 2IDX & MLSImport
  • 6Installation & Setup
  • 22Installation FAQ
  • 23Maps & Location Settings
  • 21Multi-Language Third Party Plugins
  • 6Other Third party Plugins
  • 19Pages
  • 4Payments & Monetization
  • 20Property Lists, Categories & Archive
  • 36Property Pages & Layouts
  • 31Search & Filtering
  • 163Technical how to | Custom Code Required
  • 8Technical: Actions and filters
  • 6Technical: Child Theme
  • 86Theme Options & Global Settings
  • 6Translations & Languages
  • 16WPBakery Shortcodes
  • 51WPResidence / WPEstate CRM
  • 50WPResidence 5.0 Documentation
  • 8WPResidence Elementor Studio
  • 50WPResidence Translate Plugin

Join Us On

Powered by WP Estate - All Rights Reserved
  • WPRESIDENCE
  • Video Tutorials
  • Client Support
  • API