folderweb/docs/03-reference/04-internationalization.md

527 lines
12 KiB
Markdown
Raw Normal View History

# 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