Grav Multilingual Sites β Language Switcher, Translations and Fallbacks
In this tutorial, you'll learn Grav multilingual sites β configuring multiple languages, building a language switcher, managing translation workflows, setting up fallback strategies, and optimizing for multilingual SEO.
What You'll Learn
- Enabling and configuring multiple languages in Grav
- Creating translated page content
- Building a language switcher for the frontend
- Translation fallback strategies
- Multilingual SEO: hreflang tags, localized sitemaps
- Language-specific configuration and routes
Why It Matters
In WordPress, multilingual sites require plugins like WPML or Polylang. In Grav, multilingual support is built into the core. You enable languages in configuration, create translated versions of each page, and Grav handles routing, URLs, and fallbacks automatically. No third-party plugins required for basic multilingual functionality.
Real-World Use
A global documentation site serves content in English, Spanish, French, German, and Japanese. Each page has a translated version. Users see content in their browser's preferred language. The language switcher in the header lets them switch manually. SEO hreflang tags tell Google which language version to show in search results. All of this is configured in YAML β no plugins, no database.
Learning Path
flowchart LR
A["Plugin CLI"] --> B["Multilingual
β You are here"]:::current
B --> C["User Management"]
C --> D["Media Handling"]
D --> E["Grav API"]
E --> F["Web Services"]
F --> G["E-commerce with Grav"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Enabling Multiple Languages
Configure languages in user/config/system.yaml:
languages:
supported:
- en
- es
- fr
- de
- ja
default_lang: en
include_default_lang: true
pages_fallback: true
translations: true
http_accept_language: true
override_locale: true
Configuration Options
| Option | Purpose |
|---|---|
supported |
List of language codes to enable |
default_lang |
Fallback language when no translation exists |
include_default_lang |
Include language prefix in URL for default language |
pages_fallback |
Show default language content if translation missing |
http_accept_language |
Auto-detect user language from browser headers |
override_locale |
Set PHP locale for date/number formatting |
URL Structures
With include_default_lang: true:
/ β English home
/es/ β Spanish home
/fr/ β French home
/en/about β English about
/es/acerca-de β Spanish about
With include_default_lang: false:
/ β English home
/es/ β Spanish home
/about β English about (no prefix)
/es/acerca-de β Spanish about
Creating Translated Pages
Same-Folder Method
Store all language versions in the same page folder:
user/pages/02.about/
βββ default.md # English (default language)
βββ default.es.md # Spanish translation
βββ default.fr.md # French translation
βββ default.de.md # German translation
βββ default.ja.md # Japanese translation
Separate-Folder Method
Store each language in a separate page tree (useful for different content per language):
user/pages/
βββ 02.about/
β βββ default.md # English about page
βββ 02.acerca-de/
β βββ default.es.md # Spanish about page (route: /es/acerca-de)
βββ 02.a-propos/
βββ default.fr.md # French about page (route: /fr/a-propos)
Frontmatter-Only Translation
For simple fields, translate within the same frontmatter:
---
title: About Us
title@es: Sobre Nosotros
title@fr: A Propos de Nous
menu: About
menu@es: Acerca de
menu@fr: A Propos
---
Language Switcher
Create a language switcher partial:
user/themes/mytheme/templates/partials/language-switcher.html.twig:
{% if grav.language.enabled %}
<div class="language-switcher">
<span class="current-language">{{ grav.language.getActive|upper }}</span>
<ul class="language-list">
{% for language in grav.language.getLanguages %}
{% if language != grav.language.getActive %}
<li>
<a href="{{ base_url ~ '/' ~ language ~ page.route }}" hreflang="{{ language }}">
{{ language|upper }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
Include in the header template:
<header>
{% include 'partials/language-switcher.html.twig' %}
</header>
Language Names
Display full language names:
{% for language in grav.language.getLanguages %}
<li>
<a href="{{ base_url ~ '/' ~ language ~ page.route }}" hreflang="{{ language }}">
{{ language|language_name }}
</a>
</li>
{% endfor %}
Output: "English", "EspaΓ±ol", "FranΓ§ais", "Deutsch", "ζ₯ζ¬θͺ".
Translation Fallback Strategy
# system.yaml
languages:
pages_fallback: true
fallbacks:
es: en
fr: en
de: en
ja: en
With pages_fallback: true, if a page has no Spanish translation (default.es.md), Grav shows the English version. The URL still shows /es/about but the content is in English.
Without fallback, missing translations return a 404 page.
Multilingual SEO
Hreflang Tags
user/themes/mytheme/templates/partials/hreflang.html.twig:
{% if grav.language.enabled %}
{% set page_route = page.route %}
{% set languages = grav.language.getLanguages %}
{% for lang in languages %}
{% set lang_url = base_url_absolute ~ '/' ~ lang ~ page_route %}
<link rel="alternate" hreflang="{{ lang }}" href="{{ lang_url }}" />
{% endfor %}
<link rel="alternate" hreflang="x-default"
href="{{ base_url_absolute ~ page_route }}" />
{% endif %}
Include in the <head> section:
<head>
{% include 'partials/hreflang.html.twig' %}
</head>
Localized URLs
Each language can have its own URL slugs:
---
title: About Us
route@es: /acerca-de
route@fr: /a-propos
route@de: /uber-uns
---
Language-Specific Configuration
Theme Strings
<h1>{{ 'THEME_MYTHEME.WELCOME'|t }}</h1>
<p>{{ 'THEME_MYTHEME.DESCRIPTION'|t }}</p>
Language files in user/themes/mytheme/languages/:
# en.yaml
en:
THEME_MYTHEME:
WELCOME: 'Welcome to our site'
DESCRIPTION: 'This is a multilingual Grav site'
# es.yaml
es:
THEME_MYTHEME:
WELCOME: 'Bienvenido a nuestro sitio'
DESCRIPTION: 'Este es un sitio multilingue de Grav'
Plugin Strings
# user/plugins/myplugin/languages/en.yaml
en:
PLUGIN_MYPLUGIN:
SUBMIT: Submit
CANCEL: Cancel
Language Detection
public function onPluginsInitialized()
{
$active = $this->grav['language']->getActive();
$languages = $this->grav['language']->getLanguages();
if ($active === 'ja') {
// Japanese-specific logic
}
}
Learning Path
flowchart LR
A["Plugin CLI"] --> B["Multilingual
β You are here"]:::current
B --> C["User Management"]
C --> D["Media Handling"]
D --> E["Grav API"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Common Mistakes
Not including
include_default_lang: true: Without this, the default language pages have no language prefix in URLs, making it harder for search engines to distinguish language versions.Missing
pages_fallback: true: Without fallback, users get 404 errors for pages that have not been translated yet. Fallback shows the default language version gracefully.Forgetting hreflang tags: Search engines need hreflang tags to know which language version to show. Without them, you risk duplicate content penalties.
Inconsistent URL slugs across languages: Each language version should have its own URL slug translated appropriately. Using the same slug for different languages confuses users and search engines.
Not updating language switcher for new languages: When adding a new language, update the switcher template, create language files for all themes and plugins, and add hreflang entries.
Practice Questions
How do you enable multilingual support in Grav? Answer: Add a
languagessection touser/config/system.yamlwithsupportedlist,default_lang, andpages_fallbacksettings.How do you create a translated version of a page? Answer: Create a Markdown file with the language suffix:
default.es.mdfor Spanish. The file has the same folder but different content.What is the purpose of
http_accept_language? Answer: It automatically detects the user's preferred language from their browser settings and redirects to the appropriate language version of the site.How do you handle missing translations gracefully? Answer: Enable
pages_fallback: truein the language configuration. Grav shows the default language content when no translation exists for the requested page.Challenge: Build a complete multilingual site with 5 languages. Configure languages in system.yaml, create translated pages for 10 different pages (at least 3 languages per page), build a language switcher with proper hreflang attributes, set up fallback behavior for untranslated pages, add language-specific URL slugs, create language files for theme strings, and test the site with browser language auto-detection. Verify hreflang tags render correctly using Google's testing tool.
FAQ
Mini Project
Goal: Build a fully multilingual documentation site.
- Configure 4 languages (en, es, fr, ja) in system.yaml
- Create 15 pages with translations (at least 3 languages per page)
- Build a language switcher with full language names and flags
- Add hreflang tags to all pages
- Create language files for theme strings with 20+ entries each
- Set up fallback for untranslated pages
- Create language-specific URL slugs
- Add auto-detection from browser settings
- Test all language switching scenarios
- Verify SEO tags with a validation tool
What's Next
Now your site supports multiple languages. Next, learn user management:
Continue to Lesson 30: User Management β Login plugin, permissions, user accounts, and access control.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro