folderweb/docs/03-reference/04-internationalization.md
Ruben 76697e4656 Add getting started documentation
Add tutorial on adding content

Add tutorial on styling

Add tutorial on templates

Add configuration reference

Add metadata reference

Add template variables reference

Add internationalization reference

Add plugin system documentation

Add creating templates documentation

Add index page
2025-11-27 23:01:02 +01:00

12 KiB

Internationalization (i18n)

FolderWeb supports multilingual websites through the built-in languages plugin. This guide covers everything you need to build sites in multiple languages.

How It Works

The language plugin provides URL-based language selection:

  • Default language: /about/ (no language prefix)
  • Other languages: /no/om/, /de/uber-uns/

Language is determined from the URL, and content files, metadata, and translations adapt automatically.

Configuration

Enable and configure languages in custom/config.ini:

[languages]
default = "en"              # Default language (no URL prefix)
available = "en,no,de"      # Comma-separated language codes

[plugins]
enabled = "languages"       # Enable the language plugin

Language codes: Use ISO 639-1 two-letter codes (en, no, de, fr, es, etc.).

Language-Specific Content Files

Create language variants of content files using the naming pattern name.lang.ext:

content/about/
├── index.md           # Default language (English)
├── index.no.md        # Norwegian version
└── index.de.md        # German version

How it works:

  • URL /about/ → Shows index.md
  • URL /no/om/ → Shows index.no.md
  • URL /de/uber-uns/ → Shows index.de.md

Fallback behavior: If no language-specific file exists, the default file is shown.

Multiple Files Per Page

Language variants work with multiple content files:

content/portfolio/
├── 00-hero.php
├── 00-hero.no.php
├── 01-intro.md
├── 01-intro.no.md
├── 02-projects.html
└── 02-projects.no.html
  • URL /portfolio/ → Shows 00-hero.php + 01-intro.md + 02-projects.html
  • URL /no/portfolio/ → Shows 00-hero.no.php + 01-intro.no.md + 02-projects.no.html

Language-Specific Metadata

Override metadata fields for each language using sections in metadata.ini:

# Default (English)
title = "About Us"
summary = "Learn about our company"
slug = "about"

# Norwegian
[no]
title = "Om oss"
summary = "Les om bedriften vår"
slug = "om"

# German
[de]
title = "Über uns"
summary = "Erfahren Sie mehr über unser Unternehmen"
slug = "uber-uns"

Supported fields:

  • title — Page/item title
  • summary — Short description
  • search_description — SEO description
  • slug — Custom URL slug

Result:

  • /about/ — Title: "About Us"
  • /no/om/ — Title: "Om oss"
  • /de/uber-uns/ — Title: "Über uns"

Translation Files

UI strings (home link, footer text, month names) are translated using language files:

custom/languages/
├── en.ini
├── no.ini
└── de.ini

Creating Translation Files

custom/languages/en.ini:

home = "Home"
footer_handcoded = "Generated in"
footer_page_time = "ms"
months = "January,February,March,April,May,June,July,August,September,October,November,December"

custom/languages/no.ini:

home = "Hjem"
footer_handcoded = "Generert på"
footer_page_time = "ms"
months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember"

custom/languages/de.ini:

home = "Startseite"
footer_handcoded = "Generiert in"
footer_page_time = "ms"
months = "Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember"

Using Translations in Templates

Access translations via the $translations variable:

<a href="/">
  <?= htmlspecialchars($translations['home'] ?? 'Home') ?>
</a>

<footer>
  <p>
    <?= htmlspecialchars($translations['footer_handcoded'] ?? 'Generated in') ?>
    <?= number_format($pageLoadTime, 4) ?>
    <?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?>
  </p>
</footer>

Adding Custom Translation Strings

Add any strings you need:

custom/languages/en.ini:

read_more = "Read more"
posted_on = "Posted on"
by_author = "by"
categories = "Categories"
tags = "Tags"

custom/languages/no.ini:

read_more = "Les mer"
posted_on = "Publisert"
by_author = "av"
categories = "Kategorier"
tags = "Tagger"

Use in templates:

<a href="<?= $item['url'] ?>">
  <?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?></a>

<p>
  <?= htmlspecialchars($translations['posted_on'] ?? 'Posted on') ?>
  <?= $item['formatted_date'] ?>
</p>

Language Switcher

The language plugin automatically provides language switcher URLs in the $languageUrls variable.

In base.php:

<?php if (!empty($languageUrls) && count($languageUrls) > 1): ?>
  <nav class="language-switcher" aria-label="Language">
    <?php foreach ($languageUrls as $lang => $url): ?>
      <a href="<?= htmlspecialchars($url) ?>" 
         <?= ($lang === $currentLang) ? 'aria-current="true"' : '' ?>>
        <?= htmlspecialchars(strtoupper($lang)) ?>
      </a>
    <?php endforeach; ?>
  </nav>
<?php endif; ?>

How it works:

  • The switcher links to the same page in different languages
  • Language-specific slugs are automatically resolved
  • Current language is marked with aria-current="true"

Example URLs:

  • On /about/: EN → /about/, NO → /no/om/, DE → /de/uber-uns/
  • On /no/om/: EN → /about/, NO → /no/om/, DE → /de/uber-uns/

Date Formatting

Dates are automatically formatted using translated month names.

With months in language files:

# en.ini
months = "January,February,March,April,May,June,July,August,September,October,November,December"

# no.ini
months = "januar,februar,mars,april,mai,juni,juli,august,september,oktober,november,desember"

Result:

  • English: "15. December 2024"
  • Norwegian: "15. desember 2024"

Date format: [day]. [month] [year] (e.g., "15. December 2024")

Complete Multilingual Example

Directory Structure

content/
├── metadata.ini
├── index.md
├── index.no.md
└── blog/
    ├── metadata.ini
    ├── 2024-12-15-first-post/
    │   ├── metadata.ini
    │   ├── index.md
    │   ├── index.no.md
    │   └── cover.jpg
    └── 2024-12-20-second-post/
        ├── metadata.ini
        ├── index.md
        └── index.no.md

Root Metadata

content/metadata.ini:

title = "My Site"

[no]
title = "Min Side"

Blog Metadata

content/blog/metadata.ini:

title = "Blog"
summary = "Latest articles and updates"

[no]
title = "Blogg"
summary = "Siste artikler og oppdateringer"

Post Metadata

content/blog/2024-12-15-first-post/metadata.ini:

title = "My First Post"
summary = "An introduction to my blog"
slug = "first-post"

[no]
title = "Mitt første innlegg"
summary = "En introduksjon til bloggen min"
slug = "forste-innlegg"

URLs Generated

English (default):

  • Home: /
  • Blog: /blog/
  • Post: /blog/first-post/

Norwegian:

  • Home: /no/
  • Blog: /no/blogg/
  • Post: /no/blogg/forste-innlegg/

Language-Aware Navigation

Navigation menus automatically use language-specific titles:

content/about/metadata.ini:

title = "About"
menu = 1
menu_order = 10

[no]
title = "Om"

Result in navigation:

  • English site: "About"
  • Norwegian site: "Om"

Template Variables for i18n

The language plugin provides these template variables:

Variable Type Description
$currentLang String Current language code (e.g., "en", "no")
$defaultLang String Default language from config
$langPrefix String URL prefix (e.g., "", "/no")
$languageUrls Array URLs to switch languages
$translations Array Translated UI strings
$availableLangs Array All available language codes

Example usage:

<html lang="<?= htmlspecialchars($currentLang) ?>">

<a href="<?= htmlspecialchars($langPrefix) ?>/">
  <?= htmlspecialchars($translations['home'] ?? 'Home') ?>
</a>

<nav>
  <?php foreach ($navigation as $item): ?>
    <a href="<?= htmlspecialchars($langPrefix . $item['url']) ?>">
      <?= htmlspecialchars($item['title']) ?>
    </a>
  <?php endforeach; ?>
</nav>

Right-to-Left (RTL) Languages

For RTL languages (Arabic, Hebrew, etc.), set the dir attribute:

custom/templates/base.php:

<?php
$rtlLangs = ['ar', 'he', 'fa', 'ur'];
$dir = in_array($currentLang, $rtlLangs) ? 'rtl' : 'ltr';
?>
<html lang="<?= htmlspecialchars($currentLang) ?>" dir="<?= $dir ?>">

Use logical CSS properties for proper RTL support:

/* Good: logical properties */
.card {
  margin-inline-start: 1rem;
  padding-inline-end: 2rem;
}

/* Bad: directional properties */
.card {
  margin-left: 1rem;
  padding-right: 2rem;
}

Best Practices

1. Always Provide Fallbacks

<!-- Good -->
<?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?>

<!-- Bad -->
<?= htmlspecialchars($translations['read_more']) ?>

2. Use Language Codes Consistently

# Good
[languages]
available = "en,no,de"  # Lowercase, ISO 639-1

# Bad
available = "EN,nb-NO,de-DE"  # Mixed case, non-standard

3. Translate Everything

Don't mix languages on the same page:

<!-- Good -->
<p><?= htmlspecialchars($translations['posted_on']) ?> <?= $item['formatted_date'] ?></p>

<!-- Bad -->
<p>Posted on <?= $item['formatted_date'] ?></p>  <!-- English hardcoded -->

4. Test All Languages

Verify:

  • Content files load correctly
  • Metadata overrides work
  • Language switcher links are correct
  • Navigation uses translated titles
  • Dates format properly

5. Handle Missing Translations Gracefully

<?php if (isset($item['summary'])): ?>
  <p><?= htmlspecialchars($item['summary']) ?></p>
<?php else: ?>
  <p><?= htmlspecialchars($translations['no_summary'] ?? 'No description available') ?></p>
<?php endif; ?>

Limitations

No Automatic Translation

FolderWeb doesn't translate content automatically. You must:

  • Create separate content files for each language
  • Manually translate all metadata
  • Provide all translation strings

No Language Detection

FolderWeb doesn't detect browser language. Users must:

  • Click the language switcher
  • Visit a language-specific URL directly

You can add browser detection with a custom plugin if needed.

Fixed URL Structure

All languages share the same folder structure. You cannot have:

  • Content in /en/blog/ and /no/nyheter/ (different folder names)

You must use:

  • Content in /blog/ with language-specific slugs and content files

Troubleshooting

Language Switcher Shows Wrong URLs

Problem: Language switcher links to incorrect pages.

Solution: Check that language-specific slugs are set in metadata:

slug = "about"

[no]
slug = "om"  # Must be set

Content Not Changing Language

Problem: Same content appears in all languages.

Solution: Verify file naming:

  • index.no.md (correct)
  • index-no.md (wrong)
  • index_no.md (wrong)

Dates Not Translating

Problem: Dates show in English for all languages.

Solution: Add months to language files:

months = "January,February,March,April,May,June,July,August,September,October,November,December"

Navigation Shows English Titles

Problem: Menu items use English even in other languages.

Solution: Add language sections to metadata:

title = "About"
menu = 1

[no]
title = "Om"

What's Next?