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
526 lines
12 KiB
Markdown
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
|