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

526 lines
12 KiB
Markdown

# 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`:
```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`:
```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:**
```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:**
```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:**
```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:
```php
<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:**
```ini
read_more = "Read more"
posted_on = "Posted on"
by_author = "by"
categories = "Categories"
tags = "Tags"
```
**custom/languages/no.ini:**
```ini
read_more = "Les mer"
posted_on = "Publisert"
by_author = "av"
categories = "Kategorier"
tags = "Tagger"
```
Use in templates:
```php
<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
<?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:**
```ini
# 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:**
```ini
title = "My Site"
[no]
title = "Min Side"
```
### Blog Metadata
**content/blog/metadata.ini:**
```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:**
```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:**
```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:**
```php
<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
<?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:
```css
/* 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
```php
<!-- Good -->
<?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?>
<!-- Bad -->
<?= htmlspecialchars($translations['read_more']) ?>
```
### 2. Use Language Codes Consistently
```ini
# 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:
```php
<!-- 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
<?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:
```ini
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:
```ini
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:
```ini
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