9.2 KiB
How to Create a Multi-Language Site
This guide shows you how to set up and manage a multi-language website with FolderWeb.
Overview
FolderWeb supports multiple languages through:
- Language prefixes in URLs
- Language-specific content files
- Translated slugs and metadata
- Translation files for UI strings
Configuration
Step 1: Configure Available Languages
Create or edit custom/config.ini:
[languages]
default = "en"
available = "en,no,fr"
- default: The primary language (no URL prefix)
- available: Comma-separated list of all supported languages
Step 2: Create Translation Files
Create translation files for each language in custom/languages/:
mkdir -p custom/languages
English (custom/languages/en.ini):
home = "Home"
read_more = "Read more"
categories = "Categories"
tags = "Tags"
footer_text = "Made with FolderWeb"
footer_handcoded = "Generated in"
footer_page_time = "ms"
Norwegian (custom/languages/no.ini):
home = "Hjem"
read_more = "Les mer"
categories = "Kategorier"
tags = "Stikkord"
footer_text = "Laget med FolderWeb"
footer_handcoded = "Generert på"
footer_page_time = "ms"
French (custom/languages/fr.ini):
home = "Accueil"
read_more = "Lire la suite"
categories = "Catégories"
tags = "Étiquettes"
footer_text = "Créé avec FolderWeb"
footer_handcoded = "Généré en"
footer_page_time = "ms"
URL Structure
With the configuration above:
- English (default):
yoursite.com/about/ - Norwegian:
yoursite.com/no/about/ - French:
yoursite.com/fr/about/
The default language never has a URL prefix.
Creating Language-Specific Content
Method 1: Separate Files Per Language
Use language suffixes in filenames: filename.{lang}.ext
Example structure:
content/about/
├── index.md # Default language (English)
├── index.no.md # Norwegian version
└── index.fr.md # French version
Rules:
- Language-specific files (
.lang.ext) show only for that language - Default files (no language suffix) show only if no language variant exists
- Files are automatically filtered based on current language
Example Content
content/about/index.md (English):
# About Us
We are a company dedicated to simplicity.
content/about/index.no.md (Norwegian):
# Om Oss
Vi er et selskap dedikert til enkelhet.
content/about/index.fr.md (French):
# À Propos
Nous sommes une entreprise dédiée à la simplicité.
Now when users visit:
/about/→ Shows English (index.md)/no/about/→ Shows Norwegian (index.no.md)/fr/about/→ Shows French (index.fr.md)
Method 2: Language-Specific Folders
For blog posts and articles, you can create separate folders:
content/blog/
├── 2025-11-01-english-post/
│ └── index.md
├── 2025-11-01-norsk-innlegg/
│ └── index.no.md
└── 2025-11-01-article-francais/
└── index.fr.md
Translated Slugs and Titles
Use metadata.ini to provide translated slugs and metadata:
content/about/metadata.ini:
; Default (English)
title = "About Us"
slug = "about"
[no]
title = "Om Oss"
slug = "om-oss"
[fr]
title = "À Propos"
slug = "a-propos"
Now URLs become:
- English:
/about/ - Norwegian:
/no/om-oss/ - French:
/fr/a-propos/
The actual folder is still named about/, but FolderWeb maps the translated slug to the real folder.
Blog Posts with Translations
Structure:
content/blog/
└── 2025-11-02-my-post/
├── index.md
├── index.no.md
├── index.fr.md
├── cover.jpg
└── metadata.ini
metadata.ini:
; Default language
title = "My First Post"
summary = "An introduction to multilingual blogging."
date = "2025-11-02"
[no]
title = "Mitt Første Innlegg"
summary = "En introduksjon til flerspråklig blogging."
[fr]
title = "Mon Premier Article"
summary = "Une introduction au blogging multilingue."
Important: Date is global, cover image is shared across languages.
Navigation and Menus
Navigation is automatically built with translations. In metadata.ini for each top-level directory:
content/blog/metadata.ini:
menu = true
menu_order = 1
title = "Blog"
[no]
title = "Blogg"
[fr]
title = "Blog"
content/about/metadata.ini:
menu = true
menu_order = 2
title = "About"
[no]
title = "Om"
[fr]
title = "À Propos"
Navigation automatically includes language prefix in URLs.
Using Translations in Templates
In Default Templates
Translations are automatically available as $translations array:
<a href="<?= $ctx->langPrefix ?>/">
<?= $translations['home'] ?>
</a>
<p><?= $translations['footer_text'] ?></p>
In Custom Templates
Access translations the same way:
<button><?= $translations['read_more'] ?></button>
Access Current Language
<?php if ($ctx->currentLang === 'no'): ?>
<p>Dette er norsk innhold.</p>
<?php else: ?>
<p>This is English content.</p>
<?php endif; ?>
Language Switcher
Create a language switcher in your custom base template:
<nav class="language-switcher">
<?php foreach ($ctx->availableLangs as $lang): ?>
<?php
$url = $lang === $ctx->defaultLang
? '/' . trim($ctx->requestPath, '/')
: '/' . $lang . '/' . trim($ctx->requestPath, '/');
$current = $lang === $ctx->currentLang;
?>
<a href="<?= $url ?>"
<?= $current ? 'aria-current="true"' : '' ?>
class="<?= $current ? 'active' : '' ?>">
<?= strtoupper($lang) ?>
</a>
<?php endforeach; ?>
</nav>
Style it:
.language-switcher {
display: flex;
gap: 0.5rem;
}
.language-switcher a {
padding: 0.25rem 0.75rem;
border-radius: var(--border-radius);
text-decoration: none;
}
.language-switcher a.active {
background: var(--color-primary);
color: white;
}
List Views with Multiple Languages
When displaying blog listings, FolderWeb automatically filters items by language:
content/blog/
├── 2025-11-01-english-article/
│ └── index.md # Shows in English
├── 2025-11-02-norsk-artikkel/
│ └── index.no.md # Shows only in Norwegian
└── 2025-11-03-universal/
├── index.md # Shows in English
├── index.no.md # Shows in Norwegian
└── index.fr.md # Shows in French
When viewing /blog/:
- Shows "english-article" and "universal"
When viewing /no/blog/:
- Shows "norsk-artikkel" and "universal"
When viewing /fr/blog/:
- Shows only "universal"
Handling Missing Translations
Default Fallback
If a translation is missing, FolderWeb uses the default language automatically.
Show Different Content
You can use PHP in your content files:
<?php if ($ctx->currentLang === 'en'): ?>
# Welcome
This page is only in English.
<?php else: ?>
# Under Construction
This page is not yet translated.
<?php endif; ?>
SEO Considerations
Add hreflang Tags
In your custom base template:
<head>
<!-- ... other head content ... -->
<?php foreach ($ctx->availableLangs as $lang): ?>
<?php
$url = $lang === $ctx->defaultLang
? 'https://yoursite.com/' . trim($ctx->requestPath, '/')
: 'https://yoursite.com/' . $lang . '/' . trim($ctx->requestPath, '/');
?>
<link rel="alternate" hreflang="<?= $lang ?>" href="<?= $url ?>">
<?php endforeach; ?>
<link rel="alternate" hreflang="x-default"
href="https://yoursite.com/<?= trim($ctx->requestPath, '/') ?>">
</head>
Language-Specific Metadata
Add language attributes:
<html lang="<?= $ctx->currentLang ?>">
Testing Your Multi-Language Site
- Visit default language:
http://localhost:8000/about/ - Visit Norwegian:
http://localhost:8000/no/about/ - Visit French:
http://localhost:8000/fr/about/ - Check navigation: Ensure links include language prefix
- Test translation strings: Verify UI text changes per language
- Check blog listings: Confirm language-specific posts appear correctly
Common Patterns
Blog in Multiple Languages
Structure:
content/blog/
├── metadata.ini # List template config
└── [date]-[slug]/
├── index.{lang}.md # One file per language
├── cover.jpg # Shared assets
└── metadata.ini # Translated metadata
Documentation in Multiple Languages
Structure:
content/docs/
├── metadata.ini # Template config
├── 00-intro.md # Default language
├── 00-intro.no.md # Norwegian
├── 01-setup.md
├── 01-setup.no.md
└── ...
Mixed Content Strategy
Not everything needs translation. You can have:
- English-only blog posts (no language suffix)
- Multi-language main pages (with language suffixes)
- Shared images and assets