# Language Plugin Reference ## Overview The language plugin provides URL-based internationalization (i18n) support for FolderWeb. It enables multi-language sites with language-specific URLs, content files, slugs, and translations. ## Configuration ### Basic Setup Add language configuration to `custom/config.ini`: ```ini [languages] default = "en" # Default language (no URL prefix) available = "en,no,fr" # Comma-separated list of languages ``` Enable the plugin: ```ini [plugins] enabled = "languages" ``` ## URL Structure The plugin modifies URL behavior based on language: - **Default language**: `/about/` (no prefix) - **Other languages**: `/no/about/`, `/fr/about/` The language code is extracted from the URL and removed from the request path before routing. ## How It Works ### 1. URL Language Extraction (CONTEXT_READY Hook) When a request arrives, the plugin: 1. Reads language config from `config.ini` 2. Checks if URL starts with a language code 3. If non-default language found, removes it from path 4. Stores language data in context **Example:** ``` URL: /no/about/contact/ ↓ Language extracted: "no" Path updated to: about/contact/ Context stores: currentLang="no", langPrefix="/no" ``` **Implementation:** ```php Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config) { $defaultLang = $config['languages']['default'] ?? 'en'; $availableLangs = explode(',', $config['languages']['available'] ?? 'en'); $pathParts = explode('/', $ctx->requestPath); $currentLang = $defaultLang; // Extract language prefix if present if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs)) { if ($pathParts[0] !== $defaultLang) { $currentLang = array_shift($pathParts); // Update request path $reflection = new ReflectionProperty($ctx, 'requestPath'); $reflection->setValue($ctx, implode('/', $pathParts)); } } // Store in context $ctx->set('currentLang', $currentLang); $ctx->set('defaultLang', $defaultLang); $ctx->set('availableLangs', $availableLangs); $ctx->set('langPrefix', $currentLang !== $defaultLang ? "/$currentLang" : ''); $ctx->set('translations', loadTranslations($currentLang)); return $ctx; }); ``` ### 2. Content Filtering (PROCESS_CONTENT Hook) The plugin filters content in three ways: #### A. Metadata Merging Language-specific metadata sections are merged into base metadata: **metadata.ini:** ```ini title = "Contact" slug = "contact" [no] title = "Kontakt" slug = "kontakt" ``` When viewing Norwegian version, metadata becomes: ```php [ 'title' => 'Kontakt', 'slug' => 'kontakt', '_raw' => [...original data...] ] ``` **Implementation:** ```php if ($extraContext === 'metadata') { if (isset($data['_raw'][$currentLang])) { return array_merge($data, $data['_raw'][$currentLang]); } } ``` #### B. Content File Filtering Files follow the naming pattern: `filename.{lang}.{ext}` **Example directory:** ``` content/about/ ├── index.md # Default language ├── index.no.md # Norwegian ├── image.jpg # Shared asset └── metadata.ini ``` **Filtering rules:** 1. Language-specific files (`.lang.ext`) only show for that language 2. Default files only show if no language version exists 3. Non-content files are ignored **Implementation:** ```php if (is_array($data) && isset($data[0]['path'])) { $filtered = []; $seen = []; foreach ($files as $file) { $parts = explode('.', $file['name']); // Check for language-specific file if (count($parts) >= 3 && in_array($parts[-2], $availableLangs)) { if ($parts[-2] === $currentLang) { $filtered[] = $file; $seen[$parts[0]] = true; } continue; } // Include default file if no language version exists if (!isset($seen[$parts[0]]) && !hasLanguageVersion($dir, $parts[0])) { $filtered[] = $file; } } return $filtered; } ``` #### C. Date Formatting Dates are formatted with localized month names from translation files: **Implementation:** ```php if ($dirOrType === 'date_format') { return formatDate($data, $currentLang); } ``` ### 3. Template Variables (TEMPLATE_VARS Hook) The plugin adds language-related variables to all templates: **Variables added:** - `$currentLang` - Active language code (e.g., "no") - `$defaultLang` - Default language code from config - `$langPrefix` - URL prefix for current language (e.g., "/no" or "") - `$translations` - Array of translated strings from `.ini` files - `$availableLangs` - Array of all configured languages - `$languageUrls` - Pre-built URLs for all languages with correct slugs **Implementation:** ```php Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) { $vars['currentLang'] = $ctx->get('currentLang'); $vars['defaultLang'] = $ctx->get('defaultLang'); $vars['langPrefix'] = $ctx->get('langPrefix'); $vars['translations'] = $ctx->get('translations'); $vars['availableLangs'] = $ctx->get('availableLangs'); $vars['languageUrls'] = buildLanguageUrls($ctx, ...); return $vars; }); ``` ## Translation Files Translation files are INI files located in: - `app/default/languages/{lang}.ini` - Default translations - `custom/languages/{lang}.ini` - Custom translations (override defaults) ### Format ```ini ; English translations (en.ini) ; Language names for language switcher language_name = "English" language_name_no = "Norsk" language_name_fr = "Français" ; Navigation home = "Home" categories = "Categories" tags = "Tags" ; Footer footer_social = "Follow us on Mastodon" footer_handcoded = "This website was hand-coded by..." footer_page_time = "ms to generate this page." ; Months for date formatting months = "January,February,March,April,May,June,July,August,September,October,November,December" ``` ### Loading Logic ```php function loadTranslations(string $lang): array { $defaultFile = "app/default/languages/$lang.ini"; $customFile = "custom/languages/$lang.ini"; $translations = file_exists($defaultFile) ? parse_ini_file($defaultFile) : []; if (file_exists($customFile)) { $translations = array_merge($translations, parse_ini_file($customFile)); } return $translations; } ``` ## Language-Specific Slugs Slugs can be translated in metadata: **Routing flow:** 1. URL arrives: `/no/kontakt/` 2. Language extracted: `currentLang = "no"`, path becomes `kontakt/` 3. Router calls `resolveSlugToFolder()` to find actual folder 4. Plugin has merged Norwegian metadata: `slug = "kontakt"` 5. Folder `contact/` matches slug `kontakt` 6. Content from `contact/` folder is served **Language switcher flow:** 1. User clicks language switcher on `/no/kontakt/` 2. Plugin resolves path to folder: `kontakt` → `contact` folder 3. Loads English metadata for `contact/` folder 4. Finds English slug: `contact` 5. Generates URL: `/contact/` 6. User navigates to correct English page ## Language URLs Generation The `buildLanguageUrls()` function generates URLs for all languages: ### Process 1. **Frontpage** - Simple language prefixes 2. **Regular pages**: - Resolve current path to actual folder names - For each language, load metadata for each folder - Extract language-specific slug from metadata - Build complete path with language prefix ### Example **Current URL:** `/no/kontakt/` **Process:** 1. Resolve path: `kontakt` → folder `contact` 2. For English (`en`): - Load `contact/metadata.ini` - Extract English slug: `contact` - Build URL: `/contact/` (default lang, no prefix) 3. For Norwegian (`no`): - Load `contact/metadata.ini` - Extract Norwegian slug: `kontakt` - Build URL: `/no/kontakt/` **Result:** ```php $languageUrls = [ 'en' => '/contact/', 'no' => '/no/kontakt/' ]; ``` ## Helper Functions ### loadTranslations() Loads translation files for a specific language. ```php function loadTranslations(string $lang): array ``` **Returns:** Array of translation key-value pairs ### formatDate() Formats ISO date strings with localized month names. ```php function formatDate(string $dateString, string $lang): string ``` **Input:** `"2025-11-25"` **Output (Norwegian):** `"25. november 2025"` ### filterFilesByLanguage() Filters content files based on current language. ```php function filterFilesByLanguage(array $files, string $dir, Context $ctx): array ``` ### buildLanguageUrls() Generates URLs for all languages with correct slugs. ```php function buildLanguageUrls(Context $ctx, string $currentLang, string $defaultLang, array $availableLangs): array ``` **Returns:** `['en' => '/about/', 'no' => '/no/om/', ...]` ### resolvePath() Resolves URL path to actual folder names. ```php function resolvePath(string $path, string $contentDir): ?array ``` **Input:** `"kontakt"` **Returns:** `["contact"]` (actual folder name) ## Template Usage ### Accessing Current Language ```php
Norwegian content
``` ### Using Translations ```php = htmlspecialchars($translations['home'] ?? 'Home') ?>= $translations['footer_text'] ?>
``` ### Building Language Switcher ```php ``` ### Language-Aware Navigation ```php ``` ## Adding a New Language To add a new language (e.g., French): 1. **Update config:** ```ini [languages] default = "en" available = "en,no,fr" ``` 2. **Create translation file** (`custom/languages/fr.ini`): ```ini language_name = "Français" language_name_en = "English" language_name_no = "Norsk" home = "Accueil" months = "janvier,février,mars,..." ``` 3. **Add French content:** - Create `.fr.md` files - Add `[fr]` sections to `metadata.ini` 4. **No code changes needed** - Plugin automatically handles it ## Content Organization Patterns ### Single File Per Language ``` content/about/ ├── index.md # English ├── index.no.md # Norwegian ├── index.fr.md # French └── metadata.ini # Translated metadata ``` ### Separate Directories ``` content/blog/ ├── 2025-11-01-english-post/ │ └── index.md ├── 2025-11-01-norwegian-post/ │ └── index.no.md └── 2025-11-01-french-post/ └── index.fr.md ``` ### Mixed Strategy ``` content/ ├── about/ │ ├── index.md # Translated │ ├── index.no.md │ └── metadata.ini └── blog/ └── 2025-11-01-news/ └── index.md # English only ``` ## Performance Considerations - Translation files loaded once per request - Metadata parsed once per directory access - Content filtering happens during directory scan - URL generation cached in template variables ## Limitations - No automatic content translation - Language must be first path segment (after domain) - Default language never has URL prefix - Shared assets must be referenced relatively ## Related Files - Plugin: `app/plugins/global/languages.php` - Hook system: `app/hooks.php` - Content loading: `app/content.php` - Routing: `app/router.php` ## See Also - [Plugin System Reference](plugin-system.md) - [How to Create Multi-Language Sites](../../docs/how-to/multi-language.md) - [Configuration Reference](../../docs/reference/configuration.md)