Engineering

Internationalization in Static Sites: Serving Content in 10+ Languages

Learn how MisuJob scaled internationalization (i18n) for their static site, serving job listings in 10+ languages across Europe. A practical guide!

· Founder & Engineer · · 8 min read
A globe with language icons representing internationalization of a static website.

Building a truly global platform requires more than just translating words; it demands a robust internationalization (i18n) strategy deeply woven into the fabric of our application. At MisuJob, where we aggregate from multiple sources and processes 1M+ job listings across Europe, serving content in multiple languages isn’t just a nice-to-have, it’s a necessity for connecting talent with opportunity.

The Challenge: Scaling Internationalization in a Static Site

We chose a static site generator (SSG) for MisuJob’s marketing website and blog due to its performance, security, and ease of deployment. However, scaling i18n across 10+ languages presented unique challenges. Unlike dynamic server-side rendering, static sites require all content to be pre-built. This means generating separate HTML files for each language, multiplying the build time and complexity.

Our initial approach involved a simple directory structure: /en/, /de/, /fr/, etc., each containing language-specific content. While straightforward, this quickly became unwieldy as the site grew. Maintaining consistency across languages, managing translations, and ensuring proper SEO localization became a significant burden.

We needed a more scalable and maintainable solution. Our core requirements were:

  • SEO-friendliness: Language-specific URLs (e.g., /de/gehaltsvergleich/) are crucial for search engine rankings.
  • Maintainability: Easy to update translations and add new languages without massive code changes.
  • Performance: Fast build times and optimal website performance for all users.
  • Scalability: Able to handle a growing number of languages and content without significant performance degradation.

Our Solution: A Multi-pronged Approach

We adopted a multi-pronged approach leveraging a combination of tools and techniques to achieve our i18n goals. This included:

  1. Centralized Translation Management: Using a dedicated translation management system (TMS).
  2. Static Site Generator Integration: Deep integration with our SSG (Next.js).
  3. Localized Routing: Dynamic routing based on user locale and language preferences.
  4. Content Delivery Network (CDN): Distributing localized content globally.

1. Centralized Translation Management

Manually managing translation files (e.g., .json, .po) quickly becomes a nightmare. We opted for a TMS to streamline the translation workflow. This provided several benefits:

  • Collaboration: Multiple translators can work simultaneously on different languages.
  • Version Control: Tracks changes and ensures consistency across translations.
  • Contextualization: Translators can see the context of each string, improving accuracy.
  • Automation: Automates the process of importing, exporting, and managing translation files.

We use a TMS that integrates seamlessly with our CI/CD pipeline. When new content is added or updated, the TMS automatically detects the changes and notifies translators. Once translations are complete, they are automatically pushed to our repository.

2. Static Site Generator Integration (Next.js)

Next.js’s built-in i18n support provided a solid foundation. We configured it using the next.config.js file:

// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'de', 'fr', 'nl', 'es', 'it', 'pl', 'pt', 'da', 'fi', 'sv'],
    defaultLocale: 'en',
    localeDetection: false, // Disables automatic locale detection
  },
  // ...other configurations
};

This configuration tells Next.js which locales we support and sets the default locale to English. We disabled automatic locale detection to provide more control over the user’s language preference, allowing us to persist preferences across sessions via cookies.

We then created a custom useTranslation hook to access translated strings within our components:

// hooks/useTranslation.js
import { useRouter } from 'next/router';
import en from '../locales/en.json';
import de from '../locales/de.json';
import fr from '../locales/fr.json';
import nl from '../locales/nl.json';
import es from '../locales/es.json';
import it from '../locales/it.json';
import pl from '../locales/pl.json';
import pt from '../locales/pt.json';
import da from '../locales/da.json';
import fi from '../locales/fi.json';
import sv from '../locales/sv.json';

const translations = {
  en,
  de,
  fr,
  nl,
  es,
  it,
  pl,
  pt,
  da,
  fi,
  sv,
};

const useTranslation = () => {
  const { locale } = useRouter();

  const t = (key) => {
    if (!locale || !translations[locale] || !translations[locale][key]) {
      return key; // Fallback to the key if translation is missing
    }
    return translations[locale][key];
  };

  return { t, locale };
};

export default useTranslation;

This hook allows us to easily access translated strings in our components:

// components/MyComponent.js
import useTranslation from '../hooks/useTranslation';

const MyComponent = () => {
  const { t } = useTranslation();

  return (
    <h1>{t('welcome_message')}</h1>
  );
};

export default MyComponent;

Each language has a corresponding JSON file (e.g., locales/en.json, locales/de.json) containing the translated strings.

3. Localized Routing

Maintaining SEO-friendly URLs for each language was crucial. Next.js automatically prefixes URLs with the locale. For example, the English version of a page might be /en/blog/my-article, while the German version would be /de/blog/my-article.

However, we needed to ensure that users were automatically redirected to their preferred language based on their browser settings or previous selections. We implemented a middleware function that intercepts requests and redirects users to the appropriate locale if necessary.

// middleware.js
import { NextResponse } from 'next/server'
import acceptLanguage from 'accept-language'
import { fallbackLng, languages } from './i18n/settings'

acceptLanguage.languages(languages)

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico).*)']
}

export function middleware(req) {
  let lng
  if (req.cookies.has('NEXT_LOCALE')) lng = acceptLanguage.fromString(req.cookies.get('NEXT_LOCALE').value)
  if (!lng) lng = acceptLanguage.fromString(req.headers.get('accept-language'))
  if (!lng) lng = fallbackLng

  // Redirect if lng in path is not supported
  if (
    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}/`)) &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer'))
    const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
    const response = NextResponse.next()
    if (lngInReferer) return response.cookies.set('NEXT_LOCALE', lngInReferer)
    return response
  }

  return NextResponse.next()
}

This middleware checks for a language cookie (NEXT_LOCALE). If it exists, it uses that language. Otherwise, it uses the accept-language header from the browser. If neither is available, it defaults to our fallback language (English). It then redirects the user to the appropriate URL if necessary.

4. Content Delivery Network (CDN)

Serving static content from a CDN is essential for performance, especially for a global audience. We use a CDN that automatically caches and distributes our static assets across multiple edge locations. This ensures that users around the world can access our content quickly and reliably.

We configured our CDN to automatically invalidate the cache whenever we deploy new content. This ensures that users always see the latest version of our website.

Performance and SEO Considerations

Internationalization can significantly impact website performance and SEO. We took several steps to mitigate these risks:

  • Code Splitting: We use code splitting to load only the necessary code for each language. This reduces the initial page load time and improves overall performance.
  • Image Optimization: We optimize images for each language by using appropriate file formats and compression levels.
  • hreflang Tags: We added hreflang tags to our HTML to tell search engines which language each page is in. This helps search engines serve the correct version of the page to users based on their language preferences.

Here’s an example of how we implement hreflang tags within the <head> section of our pages:

<link rel="alternate" href="https://www.misujob.com/en/blog/my-article" hreflang="en" />
<link rel="alternate" href="https://www.misujob.com/de/blog/my-article" hreflang="de" />
<link rel="alternate" href="https://www.misujob.com/fr/blog/my-article" hreflang="fr" />
<link rel="alternate" href="https://www.misujob.com/nl/blog/my-article" hreflang="nl" />
<!-- ... more language links -->
<link rel="alternate" href="https://www.misujob.com/blog/my-article" hreflang="x-default" />

The x-default tag is used for users whose language preferences don’t match any of our supported languages. They will be served the English version by default.

The Impact: Connecting Talent Across Borders

Our i18n strategy has significantly improved our ability to connect talent with opportunity across Europe. By serving content in multiple languages, we’ve seen a significant increase in user engagement and conversions. Users are more likely to engage with content that is presented in their native language.

We’ve also seen a positive impact on our SEO. By using language-specific URLs and hreflang tags, we’ve improved our search engine rankings in multiple countries. This has resulted in more organic traffic and more qualified job seekers finding our platform.

For example, consider the search volume for “data scientist jobs” in different European languages:

LanguageSearch TermEstimated Monthly Search Volume
Englishdata scientist jobs12,000
Germandata scientist jobs4,000
Germandata scientist jobs deutschland2,000
Frenchoffres data scientist3,000
Dutchdata scientist vacatures2,500
Spanishtrabajos data scientist1,800

By targeting these language-specific keywords, we can reach a wider audience of job seekers.

Furthermore, providing localized salary insights is crucial for professionals. Here’s a comparison of average Data Scientist salaries across several European countries based on our internal data and publicly available information:

CountryAverage Annual Salary (EUR)
Germany75,000 - 95,000
Switzerland100,000 - 130,000
UK65,000 - 85,000
Netherlands60,000 - 80,000
France55,000 - 75,000
Spain45,000 - 65,000

Presenting this data in the user’s preferred language allows them to make more informed career decisions.

Conclusion

Internationalization is a complex but essential process for any platform that wants to reach a global audience. By adopting a multi-pronged approach and leveraging the right tools, we were able to successfully scale i18n across our static site and serve content in 10+ languages.

The key takeaways from our experience are:

  • Centralized translation management is crucial for maintainability and collaboration.
  • Static site generators like Next.js provide excellent i18n support.
  • Localized routing and middleware are essential for SEO and user experience.
  • CDNs are critical for performance and scalability.
  • Performance and SEO considerations should be addressed early in the development process.

By implementing these strategies, we’ve created a truly global platform that connects talent with opportunity across Europe, powered by AI-powered job matching and a commitment to providing localized experiences. This allows us to effectively serve our users and further our mission of connecting professionals with their dream jobs.

internationalization i18n static sites multilingual web development
Share
P
Pablo Inigo

Founder & Engineer

Building MisuJob - an AI-powered job matching platform processing 1M+ job listings daily.

Engineering updates

Technical deep dives delivered to your inbox.

Find your next role with AI

Upload your CV. Get matched to 50,000+ jobs. Apply to the best fits effortlessly.

Get Started Free

User

Dashboard Profile Subscription