Skip to content

Grav Theme Languages — Multilingual Strings and Translations

DodaTech Updated 2026-06-27 7 min read

In this tutorial, you'll learn Grav theme language support — creating translation files for your theme, using multilingual strings in templates, handling language direction and locale, and best practices for Internationalization.

What You'll Learn

  • Creating language files for your theme
  • Using Twig's |t filter for translated strings
  • Handling language direction (LTR/RTL) and locale settings
  • Fallback language strategies
  • Theme-specific vs system-wide translations
  • Best practices for managing translations

Why It Matters

In WordPress, theme translation uses PO/MO files and the __() function. In Grav, theme translation uses YAML language files and the |t filter in Twig. A multilingual theme works in any language without modifying templates. You write the template once with translatable strings, create a language file per language, and Grav automatically serves the right version based on the site's active language.

Real-World Use

A documentation theme is used across 6 language versions of the same site. The theme has UI strings: "Search", "Read more", "Share this article", "Table of contents", and "Related articles". Each string is wrapped in the |t filter. Language files exist for English, Spanish, French, German, Japanese, and Hindi. When a user visits the Spanish site, the navigation shows "Buscar" instead of "Search". One template, 6 languages, zero duplication.

Learning Path

flowchart LR
    A["Theme Inheritance"] --> B["Theme Languages
← You are here"]:::current B --> C["Plugin Architecture"] C --> D["Plugin Events"] D --> E["Plugin Forms"] E --> F["Plugin Admin"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Creating Language Files

Language files are YAML files in languages/ directory inside the theme:

user/themes/mytheme/languages/
├── en.yaml    # English (default)
├── es.yaml    # Spanish
├── fr.yaml    # French
├── de.yaml    # German
├── ja.yaml    # Japanese
└── hi.yaml    # Hindi

Example: English Language File

user/themes/mytheme/languages/en.yaml:

en:
    THEME_MYTHEME:
        SEARCH: Search
        READ_MORE: Read more
        SHARE_THIS: Share this article
        TABLE_OF_CONTENTS: Table of contents
        RELATED_ARTICLES: Related articles
        BACK_TO_TOP: Back to top
        PREVIOUS_PAGE: Previous page
        NEXT_PAGE: Next page
        PAGE_NOT_FOUND: Page not found
        NO_RESULTS: No results found
        SEARCH_PLACEHOLDER: Search documentation...
        COPYRIGHT_TEXT: 'All rights reserved.'
        PUBLISHED_ON: Published on
        LAST_UPDATED: Last updated
        READING_TIME: 'min read'
        BY_AUTHOR: by
        SHARE_ON_TWITTER: Share on Twitter
        SHARE_ON_LINKEDIN: Share on LinkedIn

Example: Spanish Language File

user/themes/mytheme/languages/es.yaml:

es:
    THEME_MYTHEME:
        SEARCH: Buscar
        READ_MORE: Leer más
        SHARE_THIS: Compartir este artículo
        TABLE_OF_CONTENTS: Tabla de contenido
        RELATED_ARTICLES: Artículos relacionados
        BACK_TO_TOP: Volver arriba
        PREVIOUS_PAGE: Página anterior
        NEXT_PAGE: Página siguiente
        PAGE_NOT_FOUND: Página no encontrada
        NO_RESULTS: 'No se encontraron resultados'
        SEARCH_PLACEHOLDER: Buscar en la documentación...
        COPYRIGHT_TEXT: 'Todos los derechos reservados.'
        PUBLISHED_ON: Publicado el
        LAST_UPDATED: Última actualización
        READING_TIME: 'min de lectura'
        BY_AUTHOR: por
        SHARE_ON_TWITTER: Compartir en Twitter
        SHARE_ON_LINKEDIN: Compartir en LinkedIn

Naming Convention

Use the theme name in uppercase as the prefix:

THEME_MYTHEME_SEARCH
THEME_MYTHEME_READ_MORE

This prevents conflicts with other themes or plugins that might have the same string keys.

Using Translations in Templates

Basic Translation

<h1>{{ 'THEME_MYTHEME.SEARCH'|t }}</h1>

When the site language is English, this outputs "Search". When Spanish, "Buscar".

Translation with Placeholders

<p>{{ 'THEME_MYTHEME.READING_TIME'|t({ '%minutes%': reading_time }) }}</p>

Language file:

en:
    THEME_MYTHEME:
        READING_TIME: '%minutes% min read'
es:
    THEME_MYTHEME:
        READING_TIME: '%minutes% min de lectura'

Pluralization

<p>{{ 'THEME_MYTHEME.RESULTS'|t({ '%count%': results|length }) }}</p>

While Twig's |t filter does not handle pluralization natively, you can use separate keys:

en:
    THEME_MYTHEME:
        RESULT_SINGULAR: '1 result found'
        RESULT_PLURAL: '%count% results found'
{% set count = results|length %}
<p>{{ count == 1 ? 'THEME_MYTHEME.RESULT_SINGULAR'|t : 'THEME_MYTHEME.RESULT_PLURAL'|t({ '%count%': count }) }}</p>

Language-Aware Templates

Language Direction

Some languages are written right-to-left (Arabic, Hebrew, Urdu):

<html lang="{{ grav.language.getActive ?: 'en' }}"
      dir="{{ grav.language.getLanguageDirection }}">

{% if grav.language.getLanguageDirection == 'rtl' %}
    {% do assets.addCss('theme://css/rtl.css') %}
{% endif %}

Language-Specific Content

Show or hide content based on language:

{% if grav.language.getActive == 'ja' %}
    {% include 'partials/japanese-notice.html.twig' %}
{% endif %}

Language Name

<p>{{ grav.language.getActive|language_name }}</p>
{# Output: English, Español, Français, etc. #}

Fallback Strategy

If a translation key does not exist in the active language, Grav falls back to:

  1. The default language (usually English)
  2. The raw key name (e.g., shows THEME_MYTHEME.SEARCH as text)

This means you should always define all keys in the default language file at minimum.

# system.yaml - configure languages
languages:
    supported:
        - en
        - es
        - fr
        - de
        - ja
        - hi
    default_lang: en
    fallbacks:
        es: en
        fr: en
        de: en

Theme-Specific vs System Translations

Scope Location Prefix Used For
Theme user/themes/mytheme/languages/ THEME_MYTHEME_ Theme UI strings
Plugin user/plugins/myplugin/languages/ PLUGIN_MYPLUGIN_ Plugin UI strings
System system/languages/ GRAV_ Core Grav strings

Testing Translations

{# Debug: Show current language #}
<p>Current language: {{ grav.language.getActive }}</p>

{# Debug: List all language files loaded #}
{{ dump(grav.language.getLanguages) }}

{# Debug: Check if a translation key exists #}
{{ dump('THEME_MYTHEME.READ_MORE'|t) }}

Learning Path

flowchart LR
    A["Theme Inheritance"] --> B["Theme Languages
← You are here"]:::current B --> C["Plugin Architecture"] C --> D["Plugin Events"] D --> E["Plugin Forms"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Common Mistakes

  1. Not defining all keys in the default language: If a key is missing from the default language file, it appears as raw text (e.g., THEME_MYTHEME.SEARCH). Always keep the default language file complete.

  2. Inconsistent key naming: Keys are case-sensitive. THEME_MYTHEME.SEARCH is different from theme_mytheme.search. Use a consistent naming convention in ALL CAPS with underscores.

  3. Forgetting the prefix: Without the theme prefix, your keys might conflict with plugin or system keys. Always prefix with THEME_YOURTHEMENAME_.

  4. Hardcoding UI strings in templates: Every user-facing string in templates should use |t. Hardcoded strings break on multilingual sites.

  5. Not handling RTL languages: If your theme might be used with Arabic or Hebrew, add RTL CSS and check the language direction in templates.

Practice Questions

  1. Where do theme language files live? Answer: In user/themes/YOURTHEME/languages/. Each language has a YAML file (e.g., en.yaml, es.yaml).

  2. How do you translate a string in a Twig template? Answer: Use the |t filter: {{ 'THEME_MYTHEME.SEARCH'|t }}. For strings with placeholders, pass a hash: {{ 'THEME_MYTHEME.HELLO'|t({ '%name%': user }) }}.

  3. What happens when a translation key is missing in the active language? Answer: Grav falls back to the default language. If the key is missing there too, the raw key name is displayed as text.

  4. How do you handle RTL languages in your theme? Answer: Check grav.language.getLanguageDirection in the template and adjust direction, CSS, and layout accordingly. Load an RTL-specific stylesheet if needed.

  5. Challenge: Create a fully multilingual theme with 5 language files. Include at least 20 translatable strings covering navigation, search, pagination, metadata, forms, and error messages. Create language files for English, Spanish, French, German, and Japanese. Implement a language switcher that detects the current language and switches translations. Add RTL support for Arabic (create an Arabic language file as well). Test that each language loads correctly with proper direction and formatting.

FAQ

How do I add a new language to my theme?

Create a new YAML file in the theme's languages/ directory with the language code as filename (e.g., fr.yaml for French). Define all translation keys with the same structure as the default language file.

Can I use translation keys from system or plugin language files in my theme?

Yes. Theme templates can use any registered translation key. Access system keys with GRAV.KEY_NAME and plugin keys with PLUGIN_NAME.KEY_NAME.

How do I handle pluralization in translations?

Use separate keys for singular and plural forms. Check the count in Twig and select the appropriate key. Grav's built-in |t filter does not include pluralization logic.

Can I override a parent theme's translations in a child theme?

Yes. Create a languages/ directory in the child theme with the same keys. The child theme's translations override the parent's for matching keys.

How do I debug missing translations?

Enable the debugger and check the Messages tab. Missing translations are logged. You can also use {{ dump('MY_KEY'|t) }} in the template to inspect the output.

Mini Project

Goal: Build a multilingual theme with complete translation support.

  1. Create 5 language files (en, es, fr, de, ja) with 20+ strings each
  2. Create a Twig base template that uses |t for all UI text
  3. Implement language direction detection and RTL support
  4. Create a language switcher partial
  5. Handle pluralization for search results and blog post counts
  6. Add a language-specific date format configuration
  7. Create fallback language configuration
  8. Add a |ta array translation for navigation menus
  9. Test language switching and verify all strings translate correctly
  10. Document the translation workflow for content editors

What's Next

Now your theme supports multiple languages. Next, learn plugin architecture:

Continue to Lesson 24: Plugin Architecture — Plugin folder structure, events, DI container, and plugin development.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro