WP Manifestindependent plugin directory
manifest / i18n / polylang-slug-toolkit

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.

★ 1stars
5release downloads
v2.0.3latest · 2mo ago

Install

Download polylang-slug-toolkit-2.0.2.zip From the v2.0.2 release on GitHub, Jun 10, 2026

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.zip
Updates itself. It ships its own updater (Plugin Update Checker), so new versions appear under Dashboard → Updates.

Brings 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

  1. Copy this folder into wp-content/plugins/ and activate Polylang Slug Toolkit.
  2. 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:

  1. Top-priority rewrite rules (bare + pagination + feeds where supported) so the translated path resolves to the same archive query. Injected via rewrite_rules_array after Polylang's pass, so Polylang doesn't double-prefix them.
  2. A post_type_archive_link filter so links the theme generates use the translated base per language.
  3. A pll_translation_url filter so the language switcher and hreflang alternates point at each language's real base (Polylang otherwise just swaps the language segment, giving /corredores/ for EN instead of /riders/).
  4. 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 with wp 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.

Tag
Published
Assets
Downloads
v2.0.3 latest
Aug 14, 2026 2mo ago
tag only
—
Jun 10, 2026 4mo ago
polylang-slug-toolkit-2.0.2.zip
5
Jun 10, 2026 4mo ago
tag only
—
Jun 10, 2026 4mo ago
tag only
—
Jun 10, 2026 4mo ago
tag only
—
A grade

Security

Polylang Slug Toolkit 2.0.2 · audited by WP Registry

No security issues found.

Audited release
2.0.2
Findings
none
Worst severity
—
Content hash
dd508560496604249b070bdd…

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.