Polylang Slug Toolkit WP Registry grade A. No security issues foundWP RegistryNo security issues foundOpen the reportA
Shared slugs across Polylang languages — independent, unofficial add-on for Polylang Free. Opt-in, theme-agnostic. Not affiliated with Polylang.
Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/dbrabyn/polylang-slug-toolkit/releases/download/v2.0.2/polylang-slug-toolkit-2.0.2.zipBrings Polylang Pro's slug capabilities to Polylang Free, as opt-in, independently-toggleable modules. Two routing problems, one small plugin:
| Module | What it does | Example |
|---|---|---|
| Shared slugs | Let translations share one slug across languages | /rider/woods/ and /es/rider/woods/ (same slug) |
| Translated bases | Translate archive/base segments per language | /riders/ (EN) and /es/corredores/ (ES) |
These are two sides of the same coin. A rider's name is an invariable proper noun — it should be identical in every language (shared slugs). The archive base "riders" is a common noun — it should read in Spanish (translated bases).
Unofficial / not affiliated. Independent, third-party plugin. Not affiliated with, authored by, endorsed by, or supported by WP Syntex or the Polylang team. "Polylang" is a trademark of its owner, used here only to describe compatibility. Polylang Pro is a paid product — if you need its full feature set or official support, please buy it. No Polylang Pro code was used or consulted.
Why one toolkit with modules (not one plugin per feature)
Both features are the same layer — URL routing — and rest on the same Polylang
facts: the language is set from the URL (force_lang), the language→term map, and
the default language. One plugin means one install, one release cadence, one update
token.
Isolation is preserved where it matters: each module is loaded only when it's
on. A module's PHP file isn't even required unless its toggle is enabled, so a
fatal in one module's code can't affect the other. That's the activation-isolation
of separate plugins without the duplicated infrastructure. The toolkit stays a
routing concern, deliberately separate from field/content sync.
Requirements
- WordPress 5.8+, PHP 7.4+
- Polylang (free is fine) active
- Polylang's language set from the URL — force language via a directory code
(
/es/…) or a separate domain. This is the one prerequisite Polylang Pro also requires: when the language comes from the URL it's known before the slug/base is resolved, so the rewrite is unambiguous. With language detected from content, the toolkit self-disables.
Installation & enabling
- Copy this folder into
wp-content/plugins/and activate Polylang Slug Toolkit. - Go to Settings → Reading and enable the modules you want. Each row warns you if the URL-language prerequisite isn't met.
Prefer config-as-code? Skip the checkboxes and use the filters:
add_filter('pll_shared_slugs_enable', '__return_true'); // shared slugs
add_filter('pll_base_slugs_enable', '__return_true'); // translated bases
Module: Shared slugs
Free Polylang appends -2 (or 301-redirects) when two translations want the same
slug. This module makes slug uniqueness per language on save, and resolves a
shared slug to the right translation on the query — the way Pro does it. Works for
every translated post type (flat and hierarchical), no hard-coded names.
After enabling, re-save any translations that were previously forced to a suffixed slug so they pick up the shared slug.
Module: Translated bases
Polylang Free shares one base segment across languages: a CPT with
has_archive='riders' lives at /riders/ and /es/riders/ — the base never
translates. This module translates it, config-driven. The plugin is the engine;
which bases translate is site config, supplied via a filter (put it in your theme
or an mu-plugin):
add_filter('pll_slug_toolkit_base_map', function (array $map): array {
// post type → kind → language → translated base
$map['rider']['archive']['es'] = 'corredores'; // /es/riders/ → /es/corredores/
return $map;
});
For each configured base the module wires:
- Top-priority rewrite rules (bare + pagination + feeds where supported) so the
translated path resolves to the same archive query. Injected via
rewrite_rules_arrayafter Polylang's pass, so Polylang doesn't double-prefix them. - A
post_type_archive_linkfilter so links the theme generates use the translated base per language. - A
pll_translation_urlfilter so the language switcher andhreflangalternates point at each language's real base (Polylang otherwise just swaps the language segment, giving/corredores/for EN instead of/riders/). - A 301 from the old base (
/es/riders/) to the translated one (/es/corredores/), since Polylang's original rule still resolves the old path.
The default-language base is left at its WordPress has_archive slug; only the
languages you list are translated.
Self-flushing rewrite rules
Rewrite rules only take effect after a flush. This module flushes automatically, once, whenever the routing could have changed — keyed on a token that folds both the generated rule set and the plugin version. A new release flushes itself; a mid-version edit to the base map flushes itself. No manual Permalinks re-save is needed on any environment, including production.
Scope
Only the archive base kind is wired today. The engine is built around a generic
"base" notion; single (CPT single-post base) and taxonomy (term base) are the
natural next kinds and slot into the same rule-builder. Per-term slug translation
(e.g. a news term → noticias) is a separate, data-driven problem and is not
part of this module.
Configuration reference
| Hook / option | Type | Purpose |
|---|---|---|
pll_shared_slugs_enabled (option) |
bool | Shared-slugs on/off (Settings → Reading). Default false. |
pll_base_slugs_enabled (option) |
bool | Translated-bases on/off (Settings → Reading). Default false. |
pll_shared_slugs_enable (filter) |
bool | Force shared slugs on/off in code. |
pll_base_slugs_enable (filter) |
bool | Force translated bases on/off in code. |
pll_slug_toolkit_base_map (filter) |
array | The base map: [post_type => ['archive' => [lang => base]]]. |
pll_slug_toolkit_github_token (filter) |
string | GitHub token for update checks (see Updates). |
Localization
The plugin is fully translation-ready: every user-facing string is wrapped in a
gettext call with the polylang-slug-toolkit text domain (and a literal argument,
so it's extractable). Translation files live in languages/:
polylang-slug-toolkit.pot— the template; regenerate withwp i18n make-pot . languages/polylang-slug-toolkit.pot --exclude=plugin-update-checker,languages --skip-js.polylang-slug-toolkit-es_ES.po/.mo— Spanish (Spain), shipped.polylang-slug-toolkit-fr_FR.po/.mo— French (France), shipped.
To add a language, copy the .pot to polylang-slug-toolkit-{locale}.po, translate,
and compile with msgfmt (or wp i18n make-mo languages languages). WordPress loads
the matching .mo automatically for the admin language.
Note: this localizes the plugin's own UI. It is independent of what the plugin does — the routing engine reads languages and bases from Polylang/config at runtime and contains no hard-coded locale.
Updates
Not on wordpress.org; updates itself from its GitHub repo via
Plugin Update Checker,
bundled under plugin-update-checker/. It compares the Version: header to the
latest GitHub Release tag. To publish an update: bump the Version: header (and
PLL_SLUG_TOOLKIT_VERSION), commit, then cut a GitHub Release whose tag matches.
The repo is public, so updates work with no credentials — nothing to configure.
A GitHub read token is still accepted, and is worth setting if you hit the API's unauthenticated rate limit (or if the repo is ever made private again):
// wp-config.php
define('PLL_SLUG_TOOLKIT_GITHUB_TOKEN', 'ghp_xxx');
// or via filter:
add_filter('pll_slug_toolkit_github_token', fn() => get_my_secret('pll_slug_toolkit_gh'));
Migrating from "Shared Slugs for Polylang"
This plugin is the rebrand of the standalone Shared Slugs for Polylang (v1.x). The
shared-slugs option name is unchanged (pll_shared_slugs_enabled) and the old
GitHub token constant/filter (PLL_SHARED_SLUGS_GITHUB_TOKEN /
pll_shared_slugs_github_token) are still honoured, so an existing wp-config keeps
working. The plugin folder and slug change (polylang-shared-slugs →
polylang-slug-toolkit), so the one-time migration is: deactivate the old plugin,
put the new folder in place, activate it. Settings persist (they're DB options, not
files); shared slugs stays on.
Uninstall
Deleting the plugin removes its options (pll_shared_slugs_enabled,
pll_base_slugs_enabled, and the base-rules flush token). Slugs already saved as
shared are left as-is.
Credits & prior art
The shared-slug save side was informed by grappler/polylang-slug by Ulrich Pogson (GPL-2.0+). The query-side resolution and the entire translated-bases engine are original. No Polylang Pro source was used or consulted.
License
GPL v2 or later — the same license as Polylang and as the prior art credited above.
Releases
5 releases. Each count is every asset in that release; expand a row for the breakdown.