Skip to content

Grav Multilingual Sites β€” Language Switcher, Translations and Fallbacks

DodaTech Updated 2026-06-27 7 min read

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

  1. 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.

  2. 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.

  3. Forgetting hreflang tags: Search engines need hreflang tags to know which language version to show. Without them, you risk duplicate content penalties.

  4. 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.

  5. 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

  1. How do you enable multilingual support in Grav? Answer: Add a languages section to user/config/system.yaml with supported list, default_lang, and pages_fallback settings.

  2. How do you create a translated version of a page? Answer: Create a Markdown file with the language suffix: default.es.md for Spanish. The file has the same folder but different content.

  3. 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.

  4. How do you handle missing translations gracefully? Answer: Enable pages_fallback: true in the language configuration. Grav shows the default language content when no translation exists for the requested page.

  5. 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

Can I have different content structures per language?

Yes. Use the separate-folder method where each language has its own page tree. This is useful when the content differs significantly between languages.

How do I handle language-specific date and number formatting?

Enable override_locale: true in language config. Grav sets the PHP locale based on the active language, so {{ date()|date('F j, Y') }} outputs localized month names.

Can I use different themes per language?

No, Grav uses one theme for all languages. The theme handles language differences through translation strings and conditional template logic.

How do I redirect users based on their browser language?

Enable http_accept_language: true. Grav checks the Accept-Language HTTP header and redirects to the appropriate language version on first visit.

What is the maximum number of languages Grav supports?

There is no hard limit. Grav handles any number of languages. Performance depends on the number of translations and filesystem structure.

Mini Project

Goal: Build a fully multilingual documentation site.

  1. Configure 4 languages (en, es, fr, ja) in system.yaml
  2. Create 15 pages with translations (at least 3 languages per page)
  3. Build a language switcher with full language names and flags
  4. Add hreflang tags to all pages
  5. Create language files for theme strings with 20+ entries each
  6. Set up fallback for untranslated pages
  7. Create language-specific URL slugs
  8. Add auto-detection from browser settings
  9. Test all language switching scenarios
  10. 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