What You'll Build
By the end of this tutorial, you'll have a complete multi-language site that includes:
- A language switcher in the header that navigates between language versions
- Parallel content directories for each language (
/de/,/fr/, etc.) - Proper SEO
hreflangtags linking language variants - Navigation that stays within the current language context
- URL-based language detection (e.g.,
/de/about/serves German content)
The Simple Approach
Many i18n implementations involve complex plugins, translation mappings, and fallback logic. This guide takes a different approach: assume every page exists in every language, organized in parallel directory structures. Metalsmith builds these directories naturally, no special plugins required.
This works because:
- Metalsmith treats
src/de/the same as any other content directory - URLs are predictable:
/about/becomes/de/about/ - No build-time translation mapping is needed
- AI assistants make creating translated content straightforward
Prerequisites
Before starting, make sure you have:
- TheMetalsmith2025 Structured Content Starterset up and running
- Access to the Metalsmith Components library
- Basic understanding of HTML, CSS, and JavaScript
- A code editor and terminal access
Step 1: Download and Install the Language Switcher
First, download the language-switcher component from the component library.
Download the Component Package
Visit the language-switcher reference page and click the download button at the bottom of the page. This downloads a ZIP file containing:
language-switcher.njk- The Nunjucks template macrolanguage-switcher.css- Component styleslanguage-switcher.js- Client-side language navigationmanifest.json- Component configurationREADME.md- Complete implementation guideinstall.sh- Automated installation script
Install Using the Automated Script
After downloading, move the zip file to your project root directory, then:
# Navigate to your project root
cd /path/to/your/project
# Extract the component package
unzip language-switcher.zip
# Run the installation script
./language-switcher/install.sh
The installation script will:
- Verify you're in a Metalsmith project directory
- Check for and install the required
icondependency - Copy component files to
lib/layouts/components/_partials/language-switcher/ - Copy the README.md for reference
Step 2: Create the Languages Configuration
Create a data file that defines your available languages.
Create languages.json
Create a new file at lib/data/languages.json:
{
"defaultLang": "en",
"fallbackUrl": "/404/",
"available": [
{ "code": "en", "label": "English" },
{ "code": "de", "label": "Deutsch" },
{ "code": "fr", "label": "Français" }
]
}
Configuration explained:
defaultLang- The default language code (pages at root, no URL prefix)fallbackUrl- Where to navigate when a localized page doesn't existavailable- Array of language objects with ISO 639-1codeand displaylabel
Step 3: Add the Language Switcher to Your Header
Now we'll integrate the language switcher into your header component.
Update header.njk
Open lib/layouts/components/sections/header/header.njk and add the import and macro call.
First, add the import at the top of the file:
{% from "components/_partials/language-switcher/language-switcher.njk" import languageSwitcher %}
Then add the macro call inside the header, typically in a .misc container alongside other header utilities:
<div class="misc">
{{ languageSwitcher(data.languages.available, data.languages.defaultLang, data.languages.fallbackUrl) }}
{# Other header items like dark mode toggle, etc. #}
</div>
Update header manifest.json
Open lib/layouts/components/sections/header/manifest.json and add language-switcher to the requires array:
{
"name": "header",
"type": "section",
"styles": ["header.css"],
"scripts": ["header.js"],
"requires": ["branding", "navigation", "language-switcher"]
}
Update header.css
Add the language toggle button to your button reset styles. In the .misc section of header.css:
.misc {
display: flex;
align-items: center;
gap: clamp(var(--space-3xs), 2vw, var(--space-s));
/* Reset button styles for header buttons */
button.language-toggle,
button.theme-toggle {
background: transparent;
box-shadow: none;
padding: 0;
border-radius: 0;
backdrop-filter: none;
&:hover {
transform: none;
background: transparent;
}
}
}
Step 4: Create Your Language Directories
Now create the content structure for your additional languages.
Copy the Content Tree
For each language you want to support, copy your entire source directory:
# Create German content
cp -r src/ src/de/
# Create French content
cp -r src/ src/fr/
Your directory structure should now look like:
src/
index.md
about.md
blog/
welcome-post.md
de/
index.md
about.md
blog/
welcome-post.md
fr/
index.md
about.md
blog/
welcome-post.md
Translate the Content
With AI assistance, translating content is straightforward. For each file in the language directories, translate the prose fields while keeping the structure identical.
For example, an English page:
sections:
- sectionType: text-only
text:
title: 'About Us'
prose: |
We build tools for the modern web.
Becomes in German (src/de/about.md):
sections:
- sectionType: text-only
text:
title: 'Über uns'
prose: |
Wir entwickeln Werkzeuge für das moderne Web.
The structure, layout references, and metadata stay the same - only the human-readable text changes.
Step 5: Add the SEO hreflang Filter
Search engines need to know that /about/ and /de/about/ are the same content in different languages.
Create the Filter
Open lib/nunjucks-filters/string-filters.js and add this filter:
/**
* Strip locale prefix from a path
* /de/about/ becomes /about/
* /about/ stays /about/
* @param {string} path - The URL path
* @param {Array} locales - Array of locale objects with code property
* @param {string} defaultLocale - The default locale code
* @returns {string} Path without locale prefix
*/
function stripLocalePrefix(path, locales, defaultLocale) {
if (!path || !locales) {
return path;
}
for (const locale of locales) {
const code = locale.code || locale;
if (code !== defaultLocale && path.startsWith('/' + code + '/')) {
return path.slice(code.length + 1);
}
}
return path;
}
Export the filter in the same file:
module.exports = {
// ... existing filters
stripLocalePrefix
};
Register the Filter
In your metalsmith.js where you configure Nunjucks, add the filter:
const { stripLocalePrefix } = require('./lib/nunjucks-filters/string-filters');
// In your nunjucks configuration
nunjucks.addFilter('stripLocalePrefix', stripLocalePrefix);
Step 6: Add hreflang Tags to Your Head Template
Now add the hreflang tags to tell search engines about language variants.
Update head.njk
Open lib/layouts/components/_helpers/head.njk and add the hreflang tags:
{# Language alternate links for SEO #}
{% if data.languages and data.languages.available %}
{% set basePath = urlPath | stripLocalePrefix(data.languages.available, data.languages.defaultLang) %}
{% for lang in data.languages.available %}
{% if lang.code == data.languages.defaultLang %}
<link rel="alternate" hreflang="{{ lang.code }}" href="{{ metadata.site.url }}{{ basePath }}" />
{% else %}
<link rel="alternate" hreflang="{{ lang.code }}" href="{{ metadata.site.url }}/{{ lang.code }}{{ basePath }}" />
{% endif %}
{% endfor %}
<link rel="alternate" hreflang="x-default" href="{{ metadata.site.url }}{{ basePath }}" />
{% endif %}
This generates tags like:
<link rel="alternate" hreflang="en" href="https://example.com/about/" />
<link rel="alternate" hreflang="de" href="https://example.com/de/about/" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/about/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about/" />
Step 7: Update Navigation for Language Context
Internal links should stay within the current language context.
Update Navigation Template
In your navigation template or anywhere you render internal links, prepend the current locale:
{# Detect current locale from URL #}
{% set localePrefix = '' %}
{% if data.languages %}
{% for lang in data.languages.available %}
{% if lang.code != data.languages.defaultLang and urlPath.startsWith('/' + lang.code + '/') %}
{% set localePrefix = '/' + lang.code %}
{% endif %}
{% endfor %}
{% endif %}
{# Use localePrefix when rendering links #}
<a href="{{ localePrefix }}{{ item.url }}">{{ item.label }}</a>
This ensures:
- On
/about/, links go to/blog/,/contact/, etc. - On
/de/about/, links go to/de/blog/,/de/contact/, etc.
Step 8: Build and Test
Now let's test the complete multi-language system.
Start Development Server
npm start
This builds the site and starts the development server at http://localhost:3000.
Testing Checklist
Test the following functionality:
Language Switcher:
- Visual Check - The globe icon appears in the header
- Open Dropdown - Click the globe, the language dropdown appears
- Language Selection - Click a language, you're navigated to the correct URL
- URL Structure - Verify
/de/about/shows German content - Keyboard Access - Press Escape to close the dropdown
Navigation Context:
- Default Language - On
/about/, internal links go to/blog/, etc. - Other Languages - On
/de/about/, internal links go to/de/blog/, etc. - Language Switcher - From any page, switching languages takes you to the equivalent page
SEO Tags:
- View Source - Check page source for
hreflangtags - Verify Links - Confirm all language variants are listed
- x-default - Verify the default fallback is present
Browser DevTools Check:
- Open Console and verify no JavaScript errors
- Check Network tab for proper page loads
- Verify localStorage stores the language preference
Troubleshooting
If something isn't working, here are common issues and solutions:
Language Switcher Doesn't Appear
- Verify
language-switcheris in the header'smanifest.jsonrequires array - Check that the macro is imported and called in
header.njk - Ensure
lib/data/languages.jsonexists and is valid JSON - Rebuild and clear browser cache
Clicking Language Does Nothing
- Check browser Console for JavaScript errors
- Verify the
data-default-langanddata-fallback-urlattributes are set - Ensure the language-switcher JavaScript is being bundled
Wrong Language URLs
- Verify your directory structure matches (
src/de/, notsrc/german/) - Check that language codes in
languages.jsonmatch directory names - Ensure the
defaultLangis correct (usuallyen)
hreflang Tags Missing
- Verify the
stripLocalePrefixfilter is registered - Check that
data.languagesis available in templates - Ensure the head template includes the hreflang block
Navigation Links Wrong Language
- Verify the
localePrefixdetection logic is in your navigation template - Check that
urlPathis available in the template context - Ensure you're prepending
localePrefixto all internal links
How the Language Switcher Works
Understanding the JavaScript logic helps with customization:
- URL Detection - The switcher extracts the current locale from the URL path
- Base Path Calculation - It strips any locale prefix to get the base path
- Target URL Building - It constructs the URL for the selected language
- Existence Check - It makes a HEAD request to verify the page exists
- Navigation - If the page exists, it navigates there; otherwise, it uses the fallback URL
This means the switcher works automatically once your content directories are in place.
Summary
Congratulations! You've successfully added multi-language support to your Metalsmith site. Here's what you accomplished:
- Installed the language-switcher component
- Created the languages configuration file
- Integrated the switcher into your header
- Created parallel content directories for each language
- Added the SEO hreflang filter
- Added hreflang tags to your page head
- Updated navigation to stay within language context
- Tested the complete multi-language workflow
Key Takeaways
- No special plugins needed - Metalsmith builds language directories naturally
- Parallel structure - Every page exists in every language
- Predictable URLs -
/about/becomes/de/about/ - AI-assisted translation - Use AI to translate prose fields
- SEO-friendly - hreflang tags link language variants
- Graceful fallback - Switcher handles missing pages
Related Resources
- Language Switcher Component
- Metalsmith2025 Starter Repository
- Metalsmith Components Library
- Google hreflang Documentation
Happy internationalizing!