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