Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/denmlt/trp-seo-slug-localizer/archive/refs/heads/main.zipPer-language SEO title, meta description and slug overrides for TranslatePress.
TranslatePress translates what a visitor can see. Everything a search engine reads
instead of sees — the <title>, the meta description, the URL — stays in the
source language unless the paid SEO Pack add-on is installed. Every secondary
language therefore ships the source language's titles and the source language's
URLs, which is exactly what search engines index.
This plugin stores an override per language and serves it in the places that matter.
Any language set. Nothing here is tied to a particular pair. The languages,
their codes and their URL slugs are read from TranslatePress itself, so a site
with one secondary language and a site with six behave the same: every secondary
language gets its own block on the edit screen and its own --language value on
the command line. The default language is deliberately left alone — it already
has its title, description and slug in the post itself.
What it does
| SEO title | pre_get_document_title, plus Rank Math's title, Open Graph and Twitter tags |
| Meta description | Rank Math's description, Open Graph and Twitter tags |
| Slug | permalinks, rewrite rules, and a 301 from the untranslated URL |
| Post type archives | the same three, from one settings screen |
| Taxonomies | a translated base segment, plus a default term title and description |
| hreflang & switcher | TranslatePress' own URLs are corrected to match the translated slugs |
| XML sitemap | every language version listed and annotated (Rank Math) |
| Terms | title, description and slug, on the term edit screen |
Rank Math is optional. Without it, the document title is still translated; the meta description is left to whatever SEO plugin is in charge.
Requirements
- WordPress 6.0+ (tested up to 7.1)
- PHP 8.0+
- TranslatePress 3.x
- Pretty permalinks enabled
Install
Via Composer, on Bedrock-style installs:
composer config repositories.trp-seo-slug-localizer vcs https://github.com/denmlt/trp-seo-slug-localizer
composer require dyuzhaev/trp-seo-slug-localizer
Or drop the directory into wp-content/plugins/ and activate it.
Use
One page: the TranslatePress SEO / Slug Overrides box on the edit screen, one block per secondary language. Leave a field empty to keep the original.
Archives: Settings → TRP SEO Slug Localizer.
In bulk, via WP-CLI — which is also how the same values get onto a production
database, since overrides live in the database and not in the code. ru_RU below
is just an example; pass whichever TranslatePress language code you need:
wp trpssl list --language=ru_RU
wp trpssl set 128 --language=ru_RU --title="Протезирование зубов в Запорожье" --description="…"
wp trpssl archive services --language=ru_RU --title="Услуги" --slug=uslugi
wp trpssl taxonomy job-title --language=ru_RU --slug=specialisty
wp trpssl term 4 --language=ru_RU --title="Врачи" --slug=vrachi
wp trpssl import overrides.json --dry-run
import takes a list of entries. id is a post ID, or one of the three prefixed
forms — archive:<post_type>, taxonomy:<taxonomy>, term:<term_id>:
[
{"id": 128, "language": "ru_RU", "title": "…", "description": "…"},
{"id": "archive:services", "language": "ru_RU", "title": "…", "slug": "uslugi"},
{"id": "taxonomy:job-title", "language": "ru_RU", "slug": "specialisty"},
{"id": "term:4", "language": "ru_RU", "title": "Врачи", "slug": "vrachi"}
]
For theme authors
A theme that prints its own metadata — JSON-LD, say — should ask the plugin rather than read the SEO plugin's post meta, which is stored in one language only:
$title = function_exists('trpssl_current_override')
? trpssl_current_override('seo_title')
: '';
trpssl/supported_post_types and trpssl/supported_taxonomies filter what gets
an override slot.
How the slug rewriting works
The plugin never guesses at a URL. It asks WordPress for the real permalink with
its own filters temporarily disabled, strips the install path and the language
prefix, swaps the archive prefix and the final segment for their translated
counterparts, then puts the language prefix back. Rewrite rules are registered
for those paths, and template_redirect sends a visitor from the untranslated
address to the translated one with a 301, query string and all.
TranslatePress builds its hreflang tags and its language switcher from its own
idea of a URL, so the plugin filters trp_get_url_for_language as well. Without
that, hreflang would point at an address that only answers with a 301 — and an
hreflang that disagrees with the canonical is an hreflang Google discards. The
same filter fixes the way back: from a page whose slug is translated,
TranslatePress would otherwise drop the prefix and point at a path that does not
exist in the source language.
Term URLs work the same way, in two parts. The taxonomy base — /job-title/ —
is one value per taxonomy, and each slug after it is taken from the term that
owns it, so a hierarchical taxonomy translates parents and children alike. The
base is derived from a real term link rather than from the rewrite settings, so
with_front and custom bases are accounted for.
Only posts and terms that actually carry an override are considered, and both lists are cached — the lookup runs on every front-end request.
The sitemap
Free TranslatePress lists nothing but the source language, so translated pages exist only as hreflang tags inside HTML — which a crawler has to fetch a page to see. They are never offered for discovery, and that is the slowest way to get a language version indexed.
With Rank Math present, the plugin writes the sitemap the way Google documents
multilingual sitemaps: every language version is its own <url> entry, and every
entry carries an <xhtml:link> for each version, itself included. On a site with
one secondary language that doubles the number of listed addresses.
Two things to know about Rank Math while working on this. It caches generated
sitemaps as files under uploads/rank-math/, so nothing changes on screen
until those are deleted — wp rankmath sitemap generate alone does not clear
them. And it builds the sitemap from a query of its own, handing the entry filter
a bare stdClass row rather than a WP_Post; an instanceof check there matches
nothing and silently drops every annotation.
Notes and limits
- The static front page keeps its address; its SEO overrides still apply.
- Term archives fall back to the taxonomy-wide title and description, so a taxonomy with hundreds of terms does not need hundreds of hand-written titles.
- Changing any slug schedules a rewrite flush on the next request.
- Date and author archives are out of scope.
Development
composer install
composer test # 30 unit tests, no WordPress needed
composer lint
The logic that can produce a wrong URL — path arithmetic, language resolution,
override normalisation — lives in src/ as pure functions with no WordPress
dependencies, which is what the test suite covers. The WordPress-facing classes
in includes/ wire those to hooks.
License
GPL-2.0-or-later. Author: Den Dyuzhaev.