WP Residence Help WP Residence Help

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

Language Detection & Redirects — Developer Deep Dive

1 view 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.

Screenshot: PHP error log showing language router debug output with detected language and path

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 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.

Screenshot: Browser dev-tools Elements panel showing the data-wpr-language attribute on the body tag

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.

30. 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

WP Residence Documentation

  • 01. Getting Started
    • How to Get Support
    • Get your buyer license code.
    • Use SSL / https
    • Server / Theme Requirements
  • 02. Installation & Setup
  • 03. Installation FAQ
  • 06. Search & Filtering
    • Advanced Search Display Settings
    • Advanced Search Form
    • Geolocation Search for Half Map
    • Save Search Theme Options
    • Advanced Search Colors
  • 09. Agent, Agency & Developers
  • 08. Property Pages & Layouts
  • 07. Property Lists, Categories & Archive
  • 13. WPResidence Elementor Studio
  • 10. Blog Posts & Blog List
  • 11. Shortcodes
    • Contact Form
    • Featured Agency/Developer
    • Membership Packages
    • Testimonials
    • Google Map with Property Marker
    • Listings per Agent, Agency or Developer
    • Display Categories
    • Agent List
    • Recent Items Slider
    • Recent items
    • List Properties or Articles by ID
    • Featured Agent
    • Featured Article
    • Featured Property
    • Login & Register Form
    • Icon Content Box Shortcode
  • 12. Widgets
  • 04. Theme Options & Global Settings
    • General Settings
    • User Types Settings
    • Appearance
    • Logos & Favicon
    • Header
    • Footer Style and Colors
    • Price & Currency
    • Property Custom Fields
    • Features & Amenities
    • Listing Labels
    • Theme Slider
    • Permalinks
    • Splash Page
    • Social & Contact
    • Map Settings
    • Pin Management
    • How read from file works
    • General Design Settings
    • Custom Colors Settings
    • Header Design & Colors
    • Mobile Menu Colors
    • User Dashboard Colors
    • Print PDF Design
    • Property, Agent, Blog Lists Design Settings
    • Sidebar Widget Design
    • Font management
    • How to add custom CSS
    • Custom Property Card Unit – Beta version
    • Email Management
    • Import & Export theme options
    • reCaptcha settings
    • YELP API Integration
    • iHomefinder Optima Express IDX
    • MEMBERSHIP & PAYMENT Settings
    • Property Submission Page
    • PayPal Setup
    • Stripe Setup
    • Wire Transfer Payment Method
  • 20. Translations & Languages
  • 26. FAQ
  • 10. Pages
  • 11. Header
  • 12. Footer
  • 05. Maps & Location Settings
  • 18. Payments & Monetization
  • Plugins
    • 19. Included Plugins
    • 22. Third Party Plugins – IDX Compatibility
    • 21. Third-Party Plugins – Multi-Language
    • 23. Third party Plugins – Other
  • Technical
    • 24. Technical how to | Custom Code Required
    • 25. Technical: Child Theme

Join Us On

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