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
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/→ Showsindex.md - URL
/no/om/→ Showsindex.no.md - URL
/de/uber-uns/→ Showsindex.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/→ Shows00-hero.php+01-intro.md+02-projects.html - URL
/no/portfolio/→ Shows00-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 titlesummary— Short descriptionsearch_description— SEO descriptionslug— 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?
- Configuration Reference — Configure available languages
- Metadata Reference — Set language-specific metadata
- Template Variables — Use i18n variables in templates
- Creating Plugins — Extend i18n functionality