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(), callsget_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 useremove_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 andsanitize_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_slugsbut 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.