WP Residence Help WP Residence Help

  • WpEstate
  • WPRESIDENCE
  • Video Tutorials
  • Client Support
  • API
Home / 30. WPResidence Translate Plugin / Rewrite Rules & Query Vars — Developer Deep Dive

Rewrite Rules & Query Vars — Developer Deep Dive

2 views 0

This article documents the inbound routing layer of wpresidence-translate — how the plugin teaches WordPress to recognise language-prefixed URLs, which rewrite rules and query vars it registers, and where to hook in if you are wiring up a custom endpoint or a REST route on a multi-language real estate website.

The Registration Point

All inbound routing is wired up in includes/language-router.php. The relevant call chain on every request:

wpr_translate_language_router_bootstrap()
  → add_action( 'init', 'wpr_translate_language_router_register_rewrite_rules', 20 );
  → add_filter( 'query_vars', 'wpr_translate_language_router_register_query_vars' );
  → 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( 'parse_request', 'wpr_translate_language_router_detect_language', 0 );

Priority 20 on init is intentional — post-type registrations typically sit at the default priority 10, so all CPTs (including theme types like estate_property, estate_agent, estate_agency, estate_developer) are already known by the time the plugin’s rules are added.

Registered Query Vars

wpr_translate_language_router_register_query_vars() appends two public query vars:

Query var Purpose
wpr_lang The language slug captured from the URL prefix.
wpr_lang_path The remainder of the path after the language prefix.

Both vars are transient — wpr_translate_language_router_detect_language() unsets them from $wp->query_vars before the main query runs so they never leak into CPT-specific query resolution.

Registered Rewrite Tags & Rules

For every active language (one iteration per entry returned by wpr_translate_get_active_languages()):

add_rewrite_tag( '%wpr_lang%',      '([^/]+)' );
add_rewrite_tag( '%wpr_lang_path%', '(.*)' );

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

Key properties of the generated rule:

  • Position — top ensures the language rule beats core archive rules such as date archives that would otherwise swallow a two-letter slug.
  • Optional tail — (?:/(.*))?/?$ matches /es, /es/, and /es/foo/bar/ identically.
  • Per-language, not one-size-fits-all — a single rule per language is emitted so preg_quote of non-Latin slugs remains safe.
  • Skipped for empty slugs — languages with no slug (typically the default) are skipped; they use the unprefixed path directly.

Screenshot: Terminal showing wp rewrite list output with wpr_lang rules at the top

The Two Routing Paths

The plugin actually has two ways a request is associated with a language. They converge but start differently:

  1. Pre-rewrite path rewrite — wpr_translate_language_router_prepare_request_path() inspects $_SERVER['REQUEST_URI'] at setup_theme/init priority 0, detects a language prefix, rewrites the URI to remove it, and stores the matched language in $GLOBALS['wpr_translate_language_router']['matched_language']. After this, WordPress rewrite matching sees the language-free path and resolves normally.
  2. Rewrite-rule path — if the URI rewrite did not run (filters forced skip, plugin loaded late) but the rewrite rule fires at parse_request, $wp->query_vars['wpr_lang'] and $wp->query_vars['wpr_lang_path'] are populated. wpr_translate_language_router_detect_language() then picks them up, resolves the language, and rewrites $_SERVER['REQUEST_URI'] post-hoc for downstream consumers.

Having both paths keeps the plugin robust against unusual request flows (early-binding plugins, custom entry points, tests that bypass setup_theme).

Flushing

Rewrite rules are flushed at three moments:

  • Activation — wpr_translate_activate_plugin() in includes/activator.php calls flush_rewrite_rules() after creating tables and seeding defaults.
  • Manual admin save — visiting Settings → Permalinks and saving triggers WordPress's own flush, which re-runs init priority 20.
  • Programmatic — any extension that adds or removes a language must call flush_rewrite_rules() after the change so the rule set reflects the new slug list.

Performance caveat: do not call flush_rewrite_rules() on every page load. The docs are emphatic on this; the plugin itself flushes only at activation.

Request URI Rewriting

wpr_translate_language_router_update_server_request_uri( $path, $query ) is the function that mutates PHP superglobals so the rest of the WordPress lifecycle sees a clean path:

$_SERVER['REQUEST_URI'] = $path . ( '' !== $query ? '?' . $query : '' );
if ( isset( $_SERVER['PATH_INFO'] ) ) {
    $_SERVER['PATH_INFO'] = $path;
}

This is load-bearing for subdirectory installs and servers that surface PATH_INFO. If you fork the routing logic, keep this helper — PHP code running later (security plugins, logging) expects the post-match URI.

Skip Contexts

Two helpers gate the router:

  • wpr_translate_language_router_should_skip_prepare() — true for is_admin(), wp_doing_ajax(), CLI, and REST requests.
  • wpr_translate_language_router_should_skip_detection() — same set, filterable independently.

Both results are filterable:

apply_filters( 'wpr_translate_language_router_skip_prepare',   $skip );
apply_filters( 'wpr_translate_language_router_skip_detection', $skip );

Use these filters to force the router to run in integration tests that dispatch via CLI.

Screenshot: PHPUnit test output with language router forced on via skip filter

Interaction With redirect_canonical

Two filters guard against redirect loops (see the Detection article for detail):

  • wpr_translate_maybe_skip_language_prefix_redirect at priority 2 — cancels redirects that only toggle a known language slug.
  • wpr_translate_maybe_skip_front_page_canonical_redirect at priority 10 — preserves /es/ for a static front page even when the Spanish translation is absent.

Both are relevant here because canonical redirects run after rewrite matching — if you add custom rewrite rules that sit alongside the language rules, make sure your canonical logic is compatible with these two filters.

Custom Endpoints — Integration Checklist

If you add your own rewrite rule for a CPT or endpoint, there is nothing special to do in most cases — the plugin rewrites the path before your rule runs, so your pattern sees a clean URI. Specifically:

  • Register your rewrite at init priority > 20 to run after the plugin’s rules, or at the default priority 10 to run before. Either works — the patterns do not overlap.
  • Do not register a rule that expects to see the language slug in the path — it has already been stripped from $_SERVER['REQUEST_URI'] by the time your rule is matched.
  • If you need the active language inside your endpoint, call wpr_translate_get_current_language(); it is set by parse_request priority 0.
  • For REST endpoints, use wpr_translate_get_context_language() — the router skips detection on REST requests by default, and this helper falls through to cookie and default.

Generating Language-Aware URLs In Reverse

The inverse of a rewrite rule is wpr_translate_language_router_get_switch_url( $language ). It reuses the parsed path, injects the target slug, and calls user_trailingslashit() to respect the site’s permalink style. Use it in widgets and programmatic language switchers rather than building URLs by string concatenation.

Gotchas

  • Do not rely on $wp->query_vars['wpr_lang'] downstream of parse_request — the router unsets it.
  • add_rewrite_rule() at top means the plugin’s rules beat most others. If you have a competing two-letter CPT slug that collides with a language slug, the language wins — rename your CPT slug.
  • Activation flush only covers the time of activation. If you rename a language slug programmatically, call flush_rewrite_rules() or instruct the site owner to save permalinks.
  • The rewrite patterns use preg_quote( $slug, '/' ) — non-Latin slugs are safe as-is, no additional encoding needed.

Related Reading

  • URL Structure & Permalinks — the outbound URL builder.
  • Language Detection & Redirects — the language resolution pipeline.
  • WP_Query Language Filtering — how the active language is applied to queries after routing.

For product context see 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