This article documents how wpresidence-translate (text domain wpr-translate) builds language-aware URLs from the moment WordPress asks for a permalink until the moment the localized string is returned to the caller. If you are integrating a custom post type, writing a child-theme override, or debugging a stray URL on a multi-language real estate website, start here.
Files Involved
| File | Role |
|---|---|
| includes/language-permalinks.php | Hooks into WordPress permalink filters and routes every generated link through the localizer. |
| includes/functions-permalinks.php | Shared URL builder — strips any existing language prefix and injects the target one. |
| includes/permalinks-helpers.php | Low-level helpers (wpr_translation_get_known_language_slugs(), wpr_translation_build_url_from_parts()). |
| includes/activator.php | Creates the {prefix}wpestate_translation_slugs table during activation. |
The Filter Stack
wpr_translate_language_permalinks_bootstrap() registers six filters at priority 10:
add_filter( 'post_type_link', 'wpr_translate_filter_post_type_link', 10, 4 ); add_filter( 'post_link', 'wpr_translate_filter_post_link', 10, 3 ); add_filter( 'page_link', 'wpr_translate_filter_page_link', 10, 3 ); add_filter( 'term_link', 'wpr_translate_filter_term_link', 10, 3 ); add_filter( 'post_type_archive_link', 'wpr_translate_filter_post_type_archive_link', 10, 2 ); add_filter( 'home_url', 'wpr_translate_filter_home_url', 10, 4 );
Each callback funnels into wpr_translate_localise_post_link() or calls wpr_translate_prefix_url_for_language() directly. The end of the chain is always wpr_translation_localize_url_for_language() in functions-permalinks.php.
Picking the Target Language
wpr_translate_get_element_language( $element_id, $element_type ) runs a prepared query against {$wpdb->prefix}wpestate_translation_translations:
SELECT language_code
FROM {prefix}wpestate_translation_translations
WHERE element_id = %d AND element_type = %s
LIMIT 1
The element_type argument is prefixed per kind:
- Posts and pages → post_{post_type} (e.g. post_estate_property, post_page).
- Taxonomy terms → tax_{taxonomy} (e.g. tax_property_city).
Results are memoised in $GLOBALS[‘wpr_translate_language_cache’] keyed by “{element_type}:{element_id}” to avoid repeated queries during a single request. Cache misses are stored as null so a second lookup short-circuits just as fast.
The URL Localizer
wpr_translation_localize_url_for_language( $url, $language ) is the heart of the system. Its algorithm:
- wp_parse_url() the incoming URL.
- Strip the home URL’s base path segments (so the helper also works in subdirectory WP installs).
- Build a known slugs array from wpr_translation_get_known_language_slugs() plus the target language’s slug and sanitised code.
- Shift off any leading segment that matches a known language slug. This makes the helper idempotent: calling it twice leaves the URL unchanged.
- If the target language is not the default, array_unshift() its slug onto the relative segments.
- Rejoin base + relative segments, respect the original trailing-slash decision, and rebuild the URL via wpr_translation_build_url_from_parts().
Home and base parts are statically cached on the first call so repeated use inside a single request is cheap.
Slug Storage for Translated Content
Per-language content slugs live in {prefix}wpestate_translation_slugs, created by wpr_translate_activator_create_tables():
| Column | Type | Notes |
|---|---|---|
| id | BIGINT UNSIGNED | Auto-increment primary key. |
| element_id | BIGINT UNSIGNED | Post or term ID. |
| element_type | VARCHAR(45) | post_* / tax_* convention, same as translations table. |
| language_code | VARCHAR(7) | Matches {prefix}wpestate_translation_languages.code. |
| slug | VARCHAR(255) | Currently active slug for this (element, language) pair. |
| old_slugs | TEXT | Serialized history of previous slugs; drives old-URL → new-URL lookups. |
| updated_at | DATETIME | ON UPDATE CURRENT_TIMESTAMP. |
Indexes: UNIQUE KEY uniq_slug (element_id, language_code) enforces one active slug per language; KEY lang (language_code) speeds per-language scans.
Generating a Permalink for a Specific Translation
wpr_translation_get_post_language_permalink( $post_id, $language_code ) is the safe helper when you need a URL for a known language — for example, to render a language switcher:
- Resolves the language payload via wpr_translate_get_language().
- Temporarily sets the current language with wpr_translate_set_current_language(), calls get_permalink(), then restores the previous language.
- Funnels the result through wpr_translation_localize_url_for_language() for a final normalisation pass.
- Includes compatibility branches for Polylang (pll_switch_language, pll_permalink) and WPML (wpml_switch_language, wpml_permalink) when either plugin is present.
Archive & Home URL Nuance
wpr_translate_filter_post_type_archive_link() and wpr_translate_filter_home_url() do not look up the element in the translations table — there is no element. They read wpr_translate_get_current_language() and fall back to wpr_translate_get_default_language(). Implication: if you call home_url() while the current language context has not yet been established (very early hooks, CLI), the URL will use the default language prefix.
Default Language Is Prefix-Free
wpr_translate_is_default_language() short-circuits slug injection. The builder still strips any stray default-language prefix from the path so defensively-formed URLs normalise correctly, but it never adds one. This is by design — the default language keeps the clean URL.
Extending the Behavior
- Add a custom post type — no action required. As long as translations are recorded with element_type = ‘post_{post_type}’, the existing filters handle URL rewriting.
- Override slug generation — copy the relevant filter in a child-theme plugin and guard it with remove_filter() before adding your own at the same priority.
- Bypass localization for a specific URL — WordPress calls home_url() and friends through filters; return your URL on an earlier priority or use remove_filter() for the duration of your call.
- Clear the per-request cache — unset the relevant key in $GLOBALS[‘wpr_translate_language_cache’] after programmatically changing a post’s language mapping during the same request.
Gotchas
- Do not call sanitize_title() on language slugs in extension code — non-Latin characters are preserved verbatim by the plugin and sanitize_title() would strip them.
- The filter chain runs on every URL WordPress emits. Be conservative with additional filters at the same priority to avoid doubling or cancelling the prefix.
- The slugs table stores history in old_slugs but the plugin does not auto-generate 301 redirects from them — that is on the rewrite layer.
Related Reading
- Language Detection & Redirects — how the active language is chosen before URL filters run.
- Rewrite Rules & Query Vars — the inbound side of the URL contract.
- Translation Linking (trid system) — how element IDs are grouped across languages.
Product context and pricing are on the multi-language real estate website page.