WP Residence Help WP Residence Help

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

Language Detection & Redirects — Developer Deep Dive

184 views 0

This article is the code-level companion to the user-facing detection guide. It walks through how WPEstate 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.

CONTENT

  • Files & Entry Points
  • Router Bootstrap
  • Request Path Preparation
  • Query Vars & Rewrite Rules
  • Language Detection on parse_request
  • AJAX Requests
  • Redirect-Loop Safeguards
  • SEO Tag Emission
  • Body Language Attribute
  • Settings
  • Extension Points
  • Gotchas
  • Related Reading

The front-end language is decided by the URL prefix only. A request without a prefix is served in the default language. In 1.0.9 the plugin has no language cookie and no browser Accept-Language matching.

Files & Entry Points

File Responsibility
includes/language-router.php Request path prep, language resolution, rewrite rules, canonical-redirect guards.
includes/language-context/resolution.php Side-effect-free policy: URL prefix → language, unprefixed → default, AJAX lang parameter, wp-admin preference.
includes/language-manager.php wpr_translate_resolve_preferred_language() – AJAX lang parameter → admin user preference → default language.
assets/js/public-ajax-language.js Appends lang=<code> to front-end admin-ajax.php (and WPResidence ajax_handler.php) requests.
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( 'do_parse_request', 'wpr_translate_router_suspend_home_url_filter', 0 );
add_action( 'parse_request', 'wpr_translate_router_restore_home_url_filter', 0 );
add_action( 'wp',            'wpr_translate_router_restore_home_url_filter', 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 );

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.

The do_parse_request / parse_request pair removes the language-prefixing home_url filter while core computes the site base path in WP::parse_request(), then adds it back. Without this, a subdirectory install would see its base path as /site/fr instead of /site and property rewrite rules would not match.

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. If no segment is left (site root), sets the default language and returns.
  4. Passes the first remaining segment to wpr_translate_language_context_resolve_frontend_prefix(). It compares the segment (lower-cased) against the slug and code of every active public language. The default language has no URL slug, so it never matches a prefix.
  5. On a match, stores the language in $GLOBALS[‘wpr_translate_language_router’][‘matched_language’], sets language_from_url to true, removes the prefix, calls wpr_translate_set_current_language(), and rewrites $_SERVER[‘REQUEST_URI’] so the rest of WordPress sees the language-free path.
  6. With no match, it sets the default language.

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 with a slug:

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. If that still returns nothing, call wpr_translate_resolve_preferred_language() (AJAX lang parameter → admin user meta → default).

Whichever branch wins, wpr_translate_set_current_language() is called with reload_theme_translations => true, WPR_TRANSLATE_CURRENT_LANGUAGE is defined as a convenience constant for templates, and wpr_lang / wpr_lang_path are removed from the query vars.

AJAX Requests

The router does not run for admin-ajax.php. To keep AJAX output (search results, property cards) in the page’s language, public-ajax-language.js appends lang=<current code> to every front-end AJAX request. wpr_translate_resolve_preferred_language() checks wp_doing_ajax() first and accepts $_REQUEST[‘lang’] when it names an active language. If you send your own AJAX requests without jQuery, add the lang parameter yourself.

The old wpr_translate_set_language_cookie AJAX action, the wpestate_translation_lang_pref cookie and wpr_translate_match_browser_language() were removed. If your page cache varies on that cookie (Varnish, WP Rocket, LiteSpeed Cache, Cloudflare), remove it from the vary configuration.

Redirect-Loop Safeguards

Two redirect_canonical filters run on different priorities:

  • wpr_translate_maybe_skip_language_prefix_redirect (priority 2) – cancels redirects that only add or remove 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 calls wpr_translate_relationship_resolve_post( $front_page_id, $code, true, ‘frontend’ ). If a distinct public translation is found, the canonical redirect is allowed through.

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.

The alternates start with an x-default entry that points to the source-language URL. When a page is shown under a language prefix but has no translation in that language, the canonical points to the source-language URL. Languages without a translation get no hreflang alternate. Terms of a Not Translatable taxonomy are listed under every language’s prefix.

Body Language Attribute

wpr_translate_print_body_language_attribute() hooks wp_body_open (priority 5), wp_footer and admin_footer (priority 1). 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

There is no detection setting. The detect_browser_language, enable_url_prefix and default_language keys are no longer seeded or read. The default language is the language flagged is_default in the Languages Manager.

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 (original_path, matched_language, language_from_url, relative_path_without_language, query_string).

Gotchas

  • REST requests skip prep and detection. If you need language context in a REST handler, call wpr_translate_get_context_language() explicitly.
  • Private languages (active but not public) never match a URL prefix on the front end; they are available in wp-admin only.
  • The two canonical filters sit at different priorities on purpose. Do not re-prioritise them without understanding the front-page case.
  • wpr_translate_language_router_log() returns immediately, so router logging is off.

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.

WPEstate 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
MLSImportWordPress IDX Plugin for MLS ListingsImport MLS listings into your WordPress website, customize your property pages, and keep your inventory updated.Start free 30-day trial →

Help Categories

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

Join Us On

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