SSG Internationalization β Building Multilingual Static Sites
In this tutorial, you will learn about SSG Internationalization. We cover key concepts, practical examples, and best practices to help you master this topic.
SSG internationalization enables static sites in multiple languages through locale-specific content files, URL structures, and build-time rendering per locale.
What You'll Learn
By the end of this tutorial, you'll understand how to structure multilingual content for SSGs, implement locale-specific URLs, manage translations, handle RTL layouts, and optimize multilingual SSG builds.
Why It Matters
A global audience expects content in their language. SSG i18n lets you serve translated pages without server overhead. Proper implementation ensures search engines index all language versions correctly.
Real-World Use
A SaaS documentation site supports English, Spanish, French, and Japanese. Each language has its own content directory, URL prefix (example.com/es/), and build step. Translations are managed in version-controlled markdown files.
SSG i18n Architecture
graph TD
A[Content Sources] --> B[Locale: en]
A --> C[Locale: es]
A --> D[Locale: fr]
A --> E[Locale: ja]
B --> F[Build per Locale]
C --> F
D --> F
E --> F
F --> G[en/]
F --> H[es/]
F --> I[fr/]
F --> J[ja/]
G --> K[CDN Deploy]
H --> K
I --> K
J --> K
K --> L[User redirected
by Accept-Language]
style F fill:#e67e22,color:#fff
style K fill:#27ae60,color:#fff
style L fill:#4a90d9,color:#fff
Content Structure
# Option 1: Folder-based locale separation
# content/
# βββ en/
# β βββ blog/
# β β βββ hello-world.md
# β βββ about.md
# β βββ _index.md
# βββ es/
# β βββ blog/
# β β βββ hola-mundo.md
# β βββ about.md
# β βββ _index.md
# βββ fr/
# βββ ...
# Option 2: Filename-based locale detection
# content/
# βββ blog/
# β βββ hello-world.en.md
# β βββ hello-world.es.md
# β βββ hello-world.fr.md
# β βββ hello-world.ja.md
# Option 3: Single file with translations
# content/
# βββ blog/
# βββ hello-world.md
# i18n/
# βββ en.yaml (translations for English)
# βββ es.yaml (translations for Spanish)
# βββ fr.yaml (translations for French)
Next.js i18n Configuration
// next.config.js β Internationalized routing
module.exports = {
i18n: {
locales: ['en', 'es', 'fr', 'ja'],
defaultLocale: 'en',
localeDetection: true,
// Domain-based localization (optional)
domains: [
{
domain: 'example.com',
defaultLocale: 'en',
},
{
domain: 'example.es',
defaultLocale: 'es',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
],
},
};
// pages/index.js β Locale-aware page
export default function Home({ locale, translations }) {
return (
<div>
<h1>{translations.welcome}</h1>
<p>{translations.description}</p>
<p>Current locale: {locale}</p>
<nav className="language-switcher">
{['en', 'es', 'fr', 'ja'].map(lang => (
<Link key={lang} href="/" locale={lang}>
{lang.toUpperCase()}
</Link>
))}
</nav>
</div>
);
}
export async function getStaticProps({ locale }) {
// Load translations for the current locale
const translations = await import(`../i18n/${locale}.json`);
return {
props: {
locale,
translations: translations.default,
},
};
}
Static Generation Per Locale
// pages/blog/[slug].js β Multilingual SSG
export default function BlogPost({ post, locale }) {
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
<div className="language-links">
{post.availableLocales.map(lang => (
<Link
key={lang}
href={`/blog/${post.slugs[lang]}`}
locale={lang}
className={lang === locale ? 'active' : ''}
>
{lang.toUpperCase()}
</Link>
))}
</div>
</article>
);
}
export async function getStaticPaths() {
const locales = ['en', 'es', 'fr', 'ja'];
const paths = [];
for (const locale of locales) {
const posts = await fetchPostsForLocale(locale);
posts.forEach(post => {
paths.push({
params: { slug: post.slug },
locale,
});
});
}
return { paths, fallback: false };
}
export async function getStaticProps({ params, locale }) {
const post = await fetchPost(params.slug, locale);
if (!post) {
return { notFound: true };
}
return {
props: { post, locale },
};
}
Hugo Multilingual Setup
# hugo.toml β Multilingual configuration
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = false
[languages]
[languages.en]
title = "My Site"
languageCode = "en-US"
languageName = "English"
weight = 1
[languages.es]
title = "Mi Sitio"
languageCode = "es-ES"
languageName = "EspaΓ±ol"
weight = 2
[languages.fr]
title = "Mon Site"
languageCode = "fr-FR"
languageName = "FranΓ§ais"
weight = 3
[languages.en.params]
description = "A multilingual static site"
[languages.es.params]
description = "Un sitio estΓ‘tico multilingΓΌe"
[languages.fr.params]
description = "Un site statique multilingue"
Language Switcher Component
// components/LanguageSwitcher.jsx
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function LanguageSwitcher() {
const router = useRouter();
const locales = [
{ code: 'en', label: 'English', flag: 'πΊπΈ' },
{ code: 'es', label: 'EspaΓ±ol', flag: 'πͺπΈ' },
{ code: 'fr', label: 'FranΓ§ais', flag: 'π«π·' },
{ code: 'ja', label: 'ζ₯ζ¬θͺ', flag: 'π―π΅' },
];
return (
<div className="language-switcher">
{locales.map(({ code, label }) => (
<Link
key={code}
href={router.asPath}
locale={code}
className={`lang-link ${router.locale === code ? 'active' : ''}`}
hrefLang={code}
>
{label}
</Link>
))}
</div>
);
}
// Also add hreflang tags for SEO
function HreflangTags() {
const router = useRouter();
const baseUrl = 'https://example.com';
return (
<head>
{router.locales.map(locale => (
<link
key={locale}
rel="alternate"
hrefLang={locale}
href={`${baseUrl}/${locale}${router.asPath}`}
/>
))}
<link rel="alternate" hrefLang="x-default" href={`${baseUrl}${router.asPath}`} />
</head>
);
}
Common Mistakes
- Not using hreflang tags. Search engines need hreflang to understand which language version to show. Missing hreflang causes duplicate content issues.
- Forgetting RTL support for Arabic/Hebrew. CSS must handle both LTR and RTL. Use logical CSS properties (margin-inline-start) instead of directional (margin-left).
- Translating only content, not URLs. URLs should be translated too. /es/acerca-de/ is better than /es/about/.
- Not handling locale detection correctly. Use Accept-Language header and user preference. Don't rely solely on IP-based detection.
- Missing fallback for untranslated content. If a page isn't translated, show the default language version instead of a 404.
Practice Questions
- How do you structure multilingual content in SSG projects?
- What is the role of hreflang tags in multilingual SEO?
- How does Next.js i18n routing handle locale-based URLs?
- How do you implement a language switcher that preserves the current page?
- How do you handle RTL languages in a primarily LTR SSG site?
Challenge: Build a multilingual blog with SSG: implement English and Spanish versions, use locale-specific URLs (/en/ and /es/), set up hreflang tags, create a language switcher component, and configure the build to generate all language versions.
FAQ
Mini Project
Create a multilingual SSG site with 3 languages: set up locale-specific content directories, configure URL routing (/en/, /es/, /fr/), implement hreflang tags, build a language switcher that preserves the current page, and generate all language versions in a single build.
What's Next
Your site is multilingual. Now optimize SSG Build Performance to keep build times fast as your site grows.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro