Skip to content

SSG Internationalization β€” Building Multilingual Static Sites

DodaTech Updated 2026-06-28 6 min read

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

  1. Not using hreflang tags. Search engines need hreflang to understand which language version to show. Missing hreflang causes duplicate content issues.
  2. 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).
  3. Translating only content, not URLs. URLs should be translated too. /es/acerca-de/ is better than /es/about/.
  4. Not handling locale detection correctly. Use Accept-Language header and user preference. Don't rely solely on IP-based detection.
  5. Missing fallback for untranslated content. If a page isn't translated, show the default language version instead of a 404.

Practice Questions

  1. How do you structure multilingual content in SSG projects?
  2. What is the role of hreflang tags in multilingual SEO?
  3. How does Next.js i18n routing handle locale-based URLs?
  4. How do you implement a language switcher that preserves the current page?
  5. 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

Can SSG sites detect user language automatically?

Yes. Use the Accept-Language HTTP header on the client side or a service worker. Redirect to the appropriate language version on first visit.

How do I translate images and media?

Use locale-specific asset directories. /images/en/hero.jpg and /images/es/hero.jpg. Reference the correct path based on the current locale.

Does multilingual SSG increase build time?

Yes. Each language adds a full build cycle. A site with 5 languages takes 5x longer to build. Use parallel builds per locale to reduce time.

How do I handle partial translations?

Mark untranslated pages in frontmatter. Show a banner indicating the content is only available in English, and link to the original.

What is the best URL structure for multilingual sites?

Subdirectory (/en/, /es/) is simplest. Subdomain (en.example.com) is also common. Domain (.com, .es, .fr) is best for country-specific content.

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