This article documents how the wpresidence-translate plugin (text domain wpr-translate) intercepts and rewrites WP_Query so every front-end query is language-aware. It covers the two main hooks (pre_get_posts and the_posts), the translations-table join, pin-query handling, taxonomy and meta adjustments, and the opt-out points. Broader context on the multilingual architecture is in the multi-language real estate website overview.
Files In Scope
| File | Role |
|---|---|
includes/query-filter.php |
Bootstrap, pre_get_posts adjuster, the_posts rewriter, request language resolver. |
includes/query-filter-helpers.php |
Helper functions for translating taxonomy term IDs/slugs and resolving translated post IDs. |
Bootstrap
wpr_translate_bootstrap_query_filters() registers the two core hooks with an idempotent static flag:
add_action( 'pre_get_posts', 'wpr_translate_filter_frontend_query', 9 );
add_filter( 'the_posts', 'wpr_translate_filter_posts_results', 10, 2 );
Priority 9 on pre_get_posts ensures adjustments happen before theme and plugin code running at priority 10.
Request Language Resolution
wpr_translate_get_request_language_code() returns a normalized language code using this fallback chain:
- Explicit AJAX
$_REQUEST['lang']resolved viawpr_translate_get_language(). $GLOBALS['wpr_translate_current_language']set by the language router.wpestate_get_current_language()legacy theme helper.- AJAX HTTP referrer first path segment.
- AJAX context post ID via
wpr_translate_get_post_language(). wpr_translate_resolve_preferred_language()(cookie / browser).- Active-languages default via
wpr_translation_get_default_language_code().
pre_get_posts Adjuster
wpr_translate_filter_frontend_query( $query ) short-circuits for:
is_admin() && ! wp_doing_ajax(),wp_doing_cron(),REST_REQUEST,- skipped post types:
elementor_library,nav_menu_item, - queries with
suppress_filters = true.
After the short-circuit gate it runs three transforms:
wpr_translate_adjust_query_post_arguments( $query, $language );
$meta_adjusted = wpr_translate_adjust_query_meta( $query, $language );
if ( ! $meta_adjusted ) {
wpr_translate_adjust_query_taxonomies( $query, $language );
}
The meta branch runs first because swapping meta values shifts the query to source-language rows; taxonomy translation is then applied at the result phase via the_posts. When meta adjustment did not happen, taxonomy term IDs/slugs are translated up-front via wpr_translate_translate_taxonomy_terms().
Translations Table Join
For every public post type in the query, the plugin builds the set of element types via wpr_translation_get_element_type( $post_type ) and then queries {$wpdb->prefix}wpestate_translation_translations directly:
-- Non-default language: whitelist
SELECT element_id FROM {prefix}wpestate_translation_translations
WHERE element_type IN (%s, %s, ...) AND language_code = %s;
-- Default language: blacklist (exclude non-default records)
SELECT element_id FROM {prefix}wpestate_translation_translations
WHERE element_type IN (%s, %s, ...) AND language_code != %s;
The result is applied as post__in (non-default) or post__not_in (default) and intersected/merged with any existing theme filters. When the whitelist is empty but translations exist for these element types, the plugin forces an empty result set via post__in = [0] to avoid leaking untranslated content. When no translations exist at all for the element types, the filter stands down so the site keeps rendering during a gradual rollout.
Singular Request Exemption
If p, page_id, pagename, or name resolves to a post whose language equals the request language (via wpr_translate_resolve_translated_post_id()), the filter sets wpr_translate_adjust_results = true and returns without restricting post__in. This ensures a direct hit on an untranslated URL still renders.
Taxonomy & Meta Adjustments
wpr_translate_translate_taxonomy_terms( $terms, $taxonomy, $field, $language )— handlesterm_idandslugfields. Resolves canonical viawpr_translate_resolve_original_term_id(), strict-maps viawpr_translate_lookup_translated_term_id_strict(). Drops unresolved mappings in non-default languages.- Meta adjustments map post IDs stored inside meta values (e.g. relationship fields) to their default-language equivalents so the SQL matches source-language rows.
the_posts Rewriter
wpr_translate_filter_posts_results( $posts, $query ) runs on every query and:
- Detects pin queries:
fields = idsANDpost_typeincludesestate_property. These are processed even without thewpr_translate_adjust_resultssentinel, because map pin requests often come from AJAX handlers that bypass thepre_get_postsfilter. - For ID-only result sets, every ID is mapped to its translated counterpart via
wpr_translate_resolve_translated_post_id(), then filtered by stored post language usingwpr_translation_resolve_post_language_code(). Unmapped posts survive only in the default language. - For full
WP_Postresult sets, each post object is swapped for its translated counterpart when available. Duplicates are removed by post ID, andfound_posts,post_count, andmax_num_pagesare realigned when all posts were loaded (posts_per_page = -1or found ≤ requested).
Translated Post Resolver
wpr_translate_resolve_translated_post_id( $post_id, $language ) uses a per-request static cache keyed post_id:language:
- Loads the translation group for the post’s element type.
- Returns the exact-language translation if present and available (
wpr_translation_is_translation_post_available()). - Falls back through source language and then returns the original ID so the caller never gets zero.
Opting Queries Out
Three safe opt-outs:
suppress_filters => trueonWP_Query/get_posts(). Makes thepre_get_postsadjuster return early. Use for backend maintenance jobs.- Skipped post types — adding a post type to the early bail-out list in
wpr_translate_filter_frontend_query()(or registering it privately with a non-public status) removes it from filtering. - Context checks — REST, cron, and admin-non-AJAX are already exempt.
Avoid setting wpr_translate_adjust_results manually — it is a sentinel flag and its semantics are implementation-internal.
Performance Notes
- The translations-table lookup uses
prefixed prepared statements with anINclause onelement_type; indexes(element_id, element_type)and(language_code)cover the hot paths. - The helper cache in
wpr_translate_resolve_translated_post_id()collapses repeated lookups during loops; combined withwp_cache_get-style results from the translation group loader, most pages hit only one or two SELECTs. - For very large property sets, consider priming the translation group cache via a warm-up function before a map or pin AJAX call.
Pin Query Details
Pin queries (map markers) use fields = ids and skip the standard wpr_translate_adjust_results sentinel path. The the_posts hook detects them separately and applies two transforms: ID translation and language filtering. Default-language requests keep unmapped IDs (so legacy listings still render); non-default requests drop any ID whose stored language does not match the target.
Gotchas
- Do not mutate
post__inafter this hook without merging with the existing list. Code running later inpre_get_postscan accidentally undo language restrictions. - AJAX language detection is fragile — send
langexplicitly in your AJAX payload for deterministic behavior. - REST is exempt by design. If you need a language-aware REST endpoint, call
wpr_translate_resolve_translated_post_id()inside your endpoint handler. - Elementor templates and nav menu items are intentionally skipped to avoid breaking the template library and menu sync subsystem.
- Queries with
suppress_filterswill see raw cross-language content. This is by design — use it for migrations, exports, and anything that must see every language at once.
Related Reading
- Taxonomy Translation —
get_termsfilter that complements the post filter. - Translation Linking (trid system) — the TRID mapping consumed by the resolver.
- Meta Sync Across Language Variants — why meta-based filters return consistent result sets.
- URL Structure & Permalinks — where the request language code originates.
For the product picture, see the multi-language real estate website landing page.