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 via wpr_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 ) – handles term_id and slug fields. Resolves canonical via wpr_translate_resolve_original_term_id(), strict-maps via wpr_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 = ids AND post_type includes estate_property. These are processed even without the wpr_translate_adjust_results sentinel, because map pin requests often come from AJAX handlers that bypass the pre_get_posts filter.
- 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 using wpr_translation_resolve_post_language_code(). Unmapped posts survive only in the default language.
- For full WP_Post result sets, each post object is swapped for its translated counterpart when available. Duplicates are removed by post ID, and found_posts, post_count, and max_num_pages are realigned when all posts were loaded (posts_per_page = -1 or 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 => true on WP_Query / get_posts(). Makes the pre_get_posts adjuster 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 an IN clause on element_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 with wp_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__in after this hook without merging with the existing list. Code running later in pre_get_posts can accidentally undo language restrictions.
- AJAX language detection is fragile – send lang explicitly 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_filters will 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_terms filter 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.