Building a multilingual WordPress site without a plugin is the right call for a specific type of project: a developer-maintained site with two or three languages, limited page count, and a preference for keeping the plugin stack lean. For anything larger — many languages, frequent content updates, non-technical editors — a free plugin like Polylang will save you hours of maintenance work without meaningfully hurting performance.
This guide assumes the plugin-free scenario: you want full control, you understand the tradeoffs, and you’re comfortable editing theme files. If you’re still deciding whether to go manual or plugin-based, the last section covers that honestly.
Step 1: Choose your URL structure before you write a single page
The URL structure is the foundational decision — you can’t change it after the fact without breaking every existing URL and its SEO equity. Get this right before creating any content.
The three options:
- Subdirectories (
yoursite.com/fr/,yoursite.com/es/) — the recommended approach for most sites. One domain, link equity consolidates, easier to set up in WordPress, and Google treats each language subdirectory as a clear signal without the overhead of separate domains or DNS. - Subdomains (
fr.yoursite.com) — Google treats these as largely separate sites. Use subdomains only if you have a technical reason (different server configs per language, for example). More DNS and redirect configuration needed. - Separate ccTLDs (
yoursite.fr,yoursite.es) — strongest geographic signal to Google, but you’re managing separate domains, separate SSL certs, and separate link profiles. Only makes sense for enterprise-level international presence.
Recommendation: subdirectories. In WordPress, implement them using Pages with a language-prefix parent page. Create a page with slug fr as the parent, then create your translated pages as children — WordPress generates yoursite.com/fr/a-propos/ automatically. The site structure decisions that affect your multilingual setup most — domain, hosting, and page hierarchy — are covered in depth in things to decide before building a WordPress site.
Step 2: Create and organize language-specific pages
With your URL structure decided, create translated versions of each page manually. The workflow:
- Build the page in your primary language first (English is standard)
- Duplicate it — WordPress doesn’t have a built-in duplicate page function, but you can copy-paste blocks in the block editor or duplicate via phpmyadmin if you’re comfortable with that
- Assign it to the correct parent page (your
/fr/parent page) - Set a language-native slug —
a-proposfor French, notabout-us-fr— so the full URL reads naturally:yoursite.com/fr/a-propos/ - Translate all content including image alt text and button labels
Keep page slugs in the target language throughout. A French URL with English slugs (/fr/about-us/) reads as English to French users and misses the localization signal. If you’re new to WordPress page creation, how to create pages in WordPress covers the basics of the block editor and page hierarchy.
Step 3: Translate theme strings with WordPress i18n
This is what most “multilingual without plugins” guides skip entirely — and it’s where the real technical work lives. Translation plugins handle this automatically. Without one, you need to use WordPress’s built-in internationalization (i18n) system to translate your theme’s hard-coded strings: navigation labels, button text, footer copy, widget labels, form placeholders.
The setup in functions.php:
function mytheme_setup() {
load_theme_textdomain('mytheme', get_template_directory() . '/languages');
}
add_action('after_setup_theme', 'mytheme_setup');
In your theme templates, replace every hard-coded string with a translation function call:
// Instead of:
echo 'Read More';
// Use:
echo __('Read More', 'mytheme');
// For direct output:
_e('Submit', 'mytheme');
// In HTML attributes:
?> <input placeholder="<?php esc_attr_e('Search...', 'mytheme'); ?>">
Once your templates use these functions, generate a `.pot` (Portable Object Template) file that catalogues every translatable string:
wp i18n make-pot . languages/mytheme.pot --domain=mytheme
Open the `.pot` file in Poedit, create a new translation for French (fr_FR), translate each string, and save — Poedit generates both fr_FR.po (readable source) and fr_FR.mo (compiled binary that WordPress reads). Place both files in your theme’s /languages/ directory.
WordPress picks the correct .mo file based on get_locale(), which reads from the site’s language setting. For a manual multilingual setup without a plugin, you’d set the site’s locale to match the language of the page being served — this requires a custom locale filter based on the URL, which is the point at which the manual approach becomes genuinely complex to maintain. Full details on WordPress theme i18n setup are in the custom WordPress theme build guide.
Step 4: Add hreflang tags in WordPress
Hreflang tags tell Google which language and region each page targets, and how pages in different languages relate to each other. Without them, Google may treat your translated pages as duplicate content rather than intentional language variants.
Add them via the wp_head action in functions.php. The simplest maintainable approach: a lookup array mapping page slugs to their translated URLs.
function mytheme_hreflang_tags() {
global $post;
if (!is_singular() || !$post) {
return;
}
// Add your page translations here
$translations = [
'about-us' => [
['en', home_url('/about-us/')],
['fr', home_url('/fr/a-propos/')],
['es', home_url('/es/sobre-nosotros/')],
],
'services' => [
['en', home_url('/services/')],
['fr', home_url('/fr/services/')],
],
// ... continue for each translated page
];
$slug = $post->post_name;
if (isset($translations[$slug])) {
foreach ($translations[$slug] as [$lang, $url]) {
printf(
'<link rel="alternate" hreflang="%s" href="%s">' . "
",
esc_attr($lang),
esc_url($url)
);
}
// x-default: points to your primary language version
printf(
'<link rel="alternate" hreflang="x-default" href="%s">' . "
",
esc_url($translations[$slug][0][1])
);
}
}
add_action('wp_head', 'mytheme_hreflang_tags');
The x-default tag is not optional — Google uses it for users whose language isn’t covered by any of your specific hreflang values. It should point to your default language version (typically English).
Important: every page referenced in a hreflang set must also link back to all the others. If your English page lists the French URL and the Spanish URL, your French page must list the English and Spanish URLs, and your Spanish page must list English and French. A one-directional hreflang set is treated as invalid by Google. For the full SEO setup around hreflang, structured data, and per-page meta, see the WordPress SEO plugins guide — Rank Math and Yoast both have dedicated hreflang management if you want to handle it at the plugin layer instead of in code.
Step 5: Add a language switcher
The simplest approach: a custom WordPress navigation menu (Appearance → Menus) with links to each language version of your key pages. This works without any PHP code and is easy for a client to maintain — they can update links in the menu editor without touching theme files.
For a more dynamic switcher that updates automatically based on the current page, add a function to your theme:
function mytheme_language_switcher() {
// Map current page slug to its translations
global $post;
$slug = $post ? $post->post_name : '';
$switcher_map = [
'about-us' => ['EN' => '/about-us/', 'FR' => '/fr/a-propos/'],
'a-propos' => ['EN' => '/about-us/', 'FR' => '/fr/a-propos/'],
'services' => ['EN' => '/services/', 'FR' => '/fr/services/'],
];
if (!isset($switcher_map[$slug])) {
return; // no switcher for untranslated pages
}
echo '<ul class="lang-switcher">';
foreach ($switcher_map[$slug] as $label => $path) {
printf(
'<li><a href="%s">%s</a></li>',
esc_url(home_url($path)),
esc_html($label)
);
}
echo '</ul>';
}
// Call in your header template:
// mytheme_language_switcher();
The maintenance cost is visible here: every time you add a translated page, you update this map (and the hreflang array from Step 4). For a site with 8-10 pages in 2 languages, this is manageable. For 30 pages in 4 languages, a plugin handles this bookkeeping automatically.
Step 6: Configure SEO for each language
Every language version of a page needs its own meta title and meta description. If you’re using Rank Math or Yoast, these are set per-page in the SEO meta box — just edit each translated page and fill in the SEO fields in the target language. Don’t leave them in English on your French pages.
Other per-language SEO considerations:
- XML sitemaps: Rank Math and Yoast generate sitemaps that include all pages regardless of language — your translated pages will be included automatically as long as they’re published
- The
langattribute: WordPress’sget_language_attributes()function outputs the correctlangattribute on the<html>element. Ensure your theme’s header template uses it:<html <?php language_attributes(); ?>>. For a manual multilingual site, you’d need a filter that sets the locale per page based on the URL prefix, so this outputs correctly per language - Translated alt text: Every image on a translated page should have alt text in that page’s language — not the English version copied over
When to use a plugin instead
The manual approach described here scales to a site with roughly 2-3 languages and under 20 pages per language, maintained by a developer. Beyond that, the maintenance burden — updating hreflang arrays, switcher maps, and locale filters every time a page is added or renamed — outweighs the performance benefit of skipping a plugin.
Two plugins that don’t have the performance profile of heavier options:
- Polylang (free): Handles translation linking, automatic hreflang generation, language switcher widget, and URL routing without the weight of WPML. The free version covers basic multilingual needs for most small-to-medium sites. It works with Rank Math and Yoast for per-language SEO fields.
- WPML ($39-199/year): The enterprise standard. Handles WooCommerce, complex translation workflows, professional translation service integrations, and advanced URL routing. Worth it for eCommerce multilingual sites or sites with non-technical editors managing translations.
The performance argument against plugins is real but smaller than commonly stated. Polylang in particular is lightweight — the meaningful performance gap is between a well-configured site and a poorly-configured one, not between manual and Polylang specifically. For context on complex WordPress sites that need specialized setups, see how building a WordPress membership site handles similar “non-default” WordPress configuration decisions.


