Multilingual WordPress with Polylang: practical implementation guide
How to implement multilingual WordPress with Polylang: configuration, SEO, hreflang, automatic translations, ACF and WooCommerce integration. Real pitfalls, code, settings.
Multilingual WordPress with Polylang
Polylang is the best multilingual plugin for WordPress. Free, open-source, actively developed. This post is a complete implementation guide β install, configuration, SEO, integrations, real problems.
Why Polylang, not WPML
The comparison I show clients:
| Feature | Polylang | WPML | |---------|----------|------| | Price (1 site, 1 year) | $0 / $99 (Pro) | $39-159 | | Price (1 site, 5 years) | $0 / $495 | $195-795 | | Custom post types | β in free | β in paid | | ACF Pro integration | β in Pro | β in full | | WooCommerce integration | β in Pro | β in full | | Automatic translations | DeepL (Pro) | Own system | | Performance | Lighter | Heavier (more query) | | PL community | Active | Smaller |
My recommendation: Polylang. WPML is 5-10x more expensive in a 5-year perspective and offers nothing Polylang does not. Exception: giant portals with >50 custom post types, where WPML has more mature integrations.
Installation and configuration
# Wp-cli (faster than GUI)
wp plugin install polylang --activate
wp plugin install polylang-pro --activate # optional
Or through WP Admin β Plugins β Add New β "Polylang" β Install β Activate.
Language settings
Languages β Languages:
Languages: Polish (pl), English (en)
URL order: /pl/ for Polish (default), /en/ for English
Hide default language URL: OFF (better for SEO)
Language switcher: ON
Key decision: subdirectory (/pl/, /en/) vs subdomain
(pl.example.com, en.example.com).
Subdirectory wins:
- Easier SEO (the whole domain builds authority)
- Easier SSL certs (one cert, not two)
- Simpler management (one hosting, one config)
Subdomain makes sense when:
- Different teams handle different language versions
- Different legal requirements (GDPR, sensitive data per country)
URL structure
In Settings β Permalinks:
Setting: Post name (/%postname%/)
Polylang prefix: /pl/, /en/
Result:
- Polish version:
example.com/pl/o-mnie/ - English version:
example.com/en/about-me/
Every post/page/CPT has a separate URL per language plus a second URL in the other language. Polylang links them automatically.
Translating content
Manual translation (WP editor)
For every post Polylang adds a meta box "Language" + a "+" button next to the title. Click "+", choose a language, write the content. Fields are synced (EN title β PL title), but content is independent.
DeepL automatic translation
Polylang Pro + DeepL API:
// wp-config.php
define('POLYLANG_DEEPL_API_KEY', 'your-deepl-key');
Workflow:
- Write a post in Polish
- Click "Translate with DeepL" in the editor
- Polylang translates the content + excerpt + meta to EN
- Native speaker review + corrections
DeepL Pro ($8/m for 500k characters) gives 80-90% quality for ENβPL. Worse for NO/FI/DK pairs. Always review for critical pages (pricing, terms, privacy policy).
SEO: hreflang, canonical, sitemap
Automatic hreflang
Polylang automatically adds hreflang in <head>:
<link rel="alternate" hreflang="pl" href="https://example.com/pl/o-mnie/" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about-me/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/pl/o-mnie/" />
That is SEO best practice for multilingual sites. Google uses hreflang to show the correct language version in results.
Verification:
# Check hreflang on the page
curl -s https://example.com/pl/o-mnie/ | grep "hreflang"
Canonical URL
Polylang sets the canonical URL to the current language version (not to the default version). That is correct SEO β every language version has its own canonical.
Sitemap (with Yoast / RankMath / SEOPress)
All three popular SEO plugins integrate with Polylang. Sitemap contains all language versions with proper hreflang.
<!-- Sitemap example -->
<url>
<loc>https://example.com/pl/o-mnie/</loc>
<xhtml:link rel="alternate" hreflang="pl" href="https://example.com/pl/o-mnie/" />
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about-me/" />
</url>
<url>
<loc>https://example.com/en/about-me/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about-me/" />
<xhtml:link rel="alternate" hreflang="pl" href="https://example.com/pl/o-mnie/" />
</url>
Check whether SEO works
# 1. hreflang in <head>
curl -s https://example.com/pl/o-mnie/ | grep hreflang
# 2. Canonical per language
curl -s https://example.com/pl/o-mnie/ | grep canonical
curl -s https://example.com/en/about-me/ | grep canonical
# 3. Search Console β International Targeting
# Check whether hreflang is recognized (can take 2-4 weeks)
Integration with ACF (custom fields)
Free Polylang: ACF custom fields are per post, not per language. That means: the field value in EN you have to enter manually in PL (or vice versa).
Polylang Pro + ACF Pro solves this: custom fields per language.
// functions.php β Polylang Pro + ACF Pro
add_filter('acf/settings/l10n', function () {
return true; // enables ACF translations
});
add_filter('acf/settings/l10n_textdomain', function () {
return 'your-textdomain';
});
Then in ACF:
// Field definition with translation
acf_add_local_field_group([
'key' => 'group_clients',
'title' => 'Clients',
'fields' => [
[
'key' => 'field_client_name',
'label' => 'Client name',
'name' => 'client_name',
'type' => 'text',
],
],
'location' => [
[
[
'param' => 'post_type',
'operator' => '==',
'value' => 'project',
],
],
],
]);
From now client_name has separate values per language. In pl you
have "Jan Kowalski", in en you have "John Smith".
Integration with WooCommerce
Free Polylang: WooCommerce products are shared (one product, two language versions). Problem: price, attributes, variants are shared β which is wrong for many stores.
Polylang Pro for WooCommerce ($99/year) gives:
- Prices per language (e.g. 99 PLN / β¬22)
- Attributes per language (color "czerwony" / "red")
- Categories per language
- Shipping zones per language
Without Pro: either you use a multi-currency plugin (e.g. WOOCS), or you accept a shared price (conversion at display rate).
Language switcher in menu
Add to Appearance β Menus:
Menu: Header
Items:
- Home (PL) β /pl/
- About (PL) β /pl/o-mnie/
- Blog (PL) β /pl/blog/
- [Polylang Language Switcher] β add as "Language Switcher"
The language switcher shows active language + flags (optionally). You can pick the style: dropdown, list, flags.
My recommendation: dropdown without flags (flags are problematic β e.g. Polish page has no single "official" flag, code-switch PL/EN with flags is politically incorrect for some markets).
Production hardening
1. Check for duplicate URLs
Bug: the same page is accessible under two URLs
(/o-mnie/ and /pl/o-mnie/). Fix:
// functions.php β redirect non-prefixed URL to default language
add_action('template_redirect', function () {
if (strpos($_SERVER['REQUEST_URI'], '/pl/') === 0 ||
strpos($_SERVER['REQUEST_URI'], '/en/') === 0) {
return; // OK
}
// No prefix β redirect to default
$default = pll_default_language();
wp_redirect(home_url("/{$default}" . $_SERVER['REQUEST_URI']), 301);
exit;
});
2. Cache per language
If you use WP Super Cache or W3 Total Cache, you must disable
cache for the page or cache per language. Polylang integrates with
most cache plugins β enable "Don't cache pages for visitors with
cookies" (Polylang sets the cookie pll_language).
For full control (Cloudflare + APO) β see my Cloudflare post. HTML cache per language is a separate URL, so cache works automatically.
3. Pre-production testing
# Test 1: all language versions are reachable
for lang in pl en; do
echo "=== /${lang}/ ==="
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/${lang}/
done
# Expected: 200, 200
# Test 2: hreflang in <head>
for lang in pl en; do
echo "=== hreflang /${lang}/ ==="
curl -s https://example.com/${lang}/ | grep hreflang | head -5
done
# Test 3: canonical per language
for lang in pl en; do
echo "=== canonical /${lang}/ ==="
curl -s https://example.com/${lang}/ | grep canonical
done
# Test 4: language switcher works
curl -s https://example.com/pl/ | grep "pll-switcher" || echo "MISSING!"
Common problems
1. After WP update some translations disappear
Polylang keeps translations in wp_term_relationships. After large
WP updates this table sometimes gets cleared. Fix: backup the
database before update, restore if something disappears.
2. SEO plugin does not recognize multilingual URLs
Yoast / RankMath require manual configuration per language. Check in SEO settings whether "Permalink" is correct per language (Polylang should do this automatically, but check manually).
3. Sitemap.xml does not have all language versions
Enable "Include all languages in sitemap" in SEO settings. Check
the sitemap in /sitemap.xml β it should have all versions with
hreflang in <xhtml:link>.
4. ACF custom fields are empty after switching language
Free Polylang + ACF (without Pro) β custom fields are shared. A value entered in PL is visible in EN. Fix: Polylang Pro + ACF Pro, or manual management per language (switch language β enter different value β save).
Real case studies
Case 1: IT company site (PL + EN, 30 subpages)
Stack: Polylang (free) + Elementor + ACF Deployment: 2 days Cost: 2000 PLN SEO: After 3 months EN keyword traffic +120% (most clients foreign)
Case 2: WooCommerce store (PL + EN + DE, 500 products)
Stack: Polylang Pro + WooCommerce + ACF Pro Deployment: 5 days Cost: 6500 PLN Features: Prices per language (PLN, EUR, GBP), attributes per language, shipping zones per country
Case 3: Educational portal (PL + UA, 1000+ articles)
Stack: Polylang Pro + custom post types + DeepL Deployment: 7 days (including WPML migration) Cost: 12000 PLN Effect: translation of 1000 articles from PL to UA in 2 weeks (DeepL + native speaker review)
What is next
If you need a multilingual WordPress implementation with Polylang β get in touch. Delivery: 2-5 days, starting from 2000 PLN.
NajczΔΕciej zadawane pytania
Polylang Pro or free Polylang?
Can I use Polylang with WPML import?
How does SEO hreflang work with Polylang?
Can I translate automatically via DeepL / Google Translate?
Related posts
- wordpress
WordPress REST API: practical use cases for custom post types
How to use WordPress REST API with custom post types in production. Concrete examples: headless CMS, mobile app backend, AI integrations. PHP + JS code ready to use.
8 min - ngo
Best open-source tools for managing NGOs remotely in 2025
Complete overview of open-source for NGOs: CiviCRM, Odoo, SuiteCRM, Kanboard, Nextcloud, Mattermost, Jitsi. Costs, features, comparisons, real implementations with Polish foundations.
7 min