Grav Theme Languages — Multilingual Strings and Translations
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
|tfilter 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:
- The default language (usually English)
- The raw key name (e.g., shows
THEME_MYTHEME.SEARCHas 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
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.Inconsistent key naming: Keys are case-sensitive.
THEME_MYTHEME.SEARCHis different fromtheme_mytheme.search. Use a consistent naming convention in ALL CAPS with underscores.Forgetting the prefix: Without the theme prefix, your keys might conflict with plugin or system keys. Always prefix with
THEME_YOURTHEMENAME_.Hardcoding UI strings in templates: Every user-facing string in templates should use
|t. Hardcoded strings break on multilingual sites.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
Where do theme language files live? Answer: In
user/themes/YOURTHEME/languages/. Each language has a YAML file (e.g.,en.yaml,es.yaml).How do you translate a string in a Twig template? Answer: Use the
|tfilter:{{ 'THEME_MYTHEME.SEARCH'|t }}. For strings with placeholders, pass a hash:{{ 'THEME_MYTHEME.HELLO'|t({ '%name%': user }) }}.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.
How do you handle RTL languages in your theme? Answer: Check
grav.language.getLanguageDirectionin the template and adjust direction, CSS, and layout accordingly. Load an RTL-specific stylesheet if needed.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
Mini Project
Goal: Build a multilingual theme with complete translation support.
- Create 5 language files (en, es, fr, de, ja) with 20+ strings each
- Create a Twig base template that uses
|tfor all UI text - Implement language direction detection and RTL support
- Create a language switcher partial
- Handle pluralization for search results and blog post counts
- Add a language-specific date format configuration
- Create fallback language configuration
- Add a
|taarray translation for navigation menus - Test language switching and verify all strings translate correctly
- 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