Skip

Fallback lang attributes

Changelog
  • I’ve moved the notice outside of the <main> element and translated it to the secondary language.

  • I’ve added concrete HTML output examples to clarify what the page methods produce for translated vs. untranslated pages. I’ve also added a separate section explaining the mixed-language problem this solution solves.

I recently stumbled over a post in the Kirby forums about the HTML lang attribute for multilingual sites.

It made me realise that I never even thought about how to handle the lang attribute when content isn’t fully translated. What happens when a visitor views a German page that falls back to English content? Let me share what I learned.

What the lang attribute does

The lang attribute helps screen readers pronounce words correctly and enables translation tools to work more reliably. While both screen readers and translation tools have automatic language detection, they’re often unreliable. That’s why we need to explicitly set this:

<html lang="en">

When dealing with mixed-language content, it’s helpful to put an additional lang attribute on specific elements. For example, let’s say you’re writing a web dev article and suddenly there’s an Eichhƶrnchen šŸæļø:

there's an <span lang="de">Eichhƶrnchen</span> šŸæļø

Without that lang="de" attribute, a screen reader would try to pronounce ā€œEichhƶrnchenā€ using English pronunciation rules. Not ideal.

How Kirby handles translations

Kirby’s language handling is quite straightforward. Once you create a translation of a page (by adding a file like default.de.txt next to your default.en.txt), the page’s content can be returned in that language. If there’s no translation, the default language will be used. You can read more about this in Kirby’s docs.

Setting the lang attribute with Kirby looks like this:

<html lang="<?= $kirby->language()->code() ?>">

This sets the lang attribute to the current language’s code. If the current language is German, you’ll get lang="de".

The mixed-language problem

Here’s where it gets tricky. What happens when a page isn’t fully translated?

Let’s say a visitor is viewing your German site, but a specific article hasn’t been translated yet. The navigation and UI elements might be in German, but the actual content falls back to English. You end up with a page that’s partially German and partially English.

The document still has <html lang="de">, but the main content is actually in English. This mismatch confuses screen readers and translation tools.

A clean solution with custom page methods

What we need is a way to set a lang attribute on our content wrapper that reflects the actual language of the content. To keep our templates clean, we can create some custom page methods.

First, a method to check if the current page is translated:

'isTranslated' => function () {
    return $this->translation(kirby()->language()->code())->exists();
}

Then, a method to return a lang attribute for the fallback language:

'fallbackLang' => function () {
    if ($this->isTranslated()) return null;
    return 'lang="' . kirby()->defaultLanguage()->code() . '"';
}

The fallbackLang() method returns null when the page is fully translated (no extra lang attribute needed), or a complete lang attribute with the default language’s code when using fallback content.

Using it in templates

Now we can use these methods in our templates:

<?php if($page->isTranslated() === false): ?>
    <p class="notice">
        <?= t('fallback.lang.notice') ?>
    </p>
<?php endif ?>

<main <?= $page->fallbackLang() ?>>
    <?= $page->text()->kt() ?>
</main>

This code produces different HTML depending on the translation status. Let’s break it down:

When viewing a fully translated German page:

<main>
    <p>Der Inhalt dieser Seite...</p>
</main>

The <main> element has no lang attribute because the content language matches the document language (<html lang="de">).

When viewing an untranslated page on the German site:

<p class="notice">
    Der Inhalt dieser Seite ist noch nicht übersetzt.
</p>

<main lang="en">
    <p>The content of this page...</p>
</main>

Here, the <main> element gets lang="en" to indicate that this section contains English content, even though the rest of the page is in German. This helps screen readers switch pronunciation rules and allows translation tools to handle the content correctly.

Final thoughts

To be honest, I haven’t worked on a multilingual site for some time, but I wanted to publish this article anyway as it’s been gathering dust in my drafts. There’s a good chance I’ll update it with some practical learnings in the future.

Have you implemented something similar on your multilingual sites? I’d love to hear about your approaches over on Mastodon.