⚠
vite-plugin-ssr has been renamed Vike, see migration guide.

Internationalization (i18n)

We can internationalize (i18n) a vite-plugin-ssr app by using a onBeforeRoute() hook.

// /renderer/_default.page.route.js

export { onBeforeRoute }

function onBeforeRoute(pageContext) {
  const { urlWithoutLocale, locale } = extractLocale(pageContext.urlOriginal)
  return {
    pageContext: {
      // We make `locale` available as `pageContext.locale`
      locale,
      // We overwrite `pageContext.urlOriginal`
      urlOriginal: urlWithoutLocale
    }
  }
}

// We can also use a library instead of implementing our own locale retrieval mechanism
function extractLocale(url) {
  // We determine the locale, for example:
  //  extractLocale('/en-US/film/42').locale === 'en-US'
  //  extractLocale('/de-DE/film/42').locale === 'de-DE'
  const locale = /* ... */

  // We remove the locale, for example:
  //  extractLocale('/en-US/film/42').urlWithoutLocale === '/film/42'
  //  extractLocale('/de-DE/film/42').urlWithoutLocale === '/film/42'
  //  ...
  urlWithoutLocale = /* ... */

  return { locale, urlWithoutLocale }
}

Upon rendering a page, onBeforeRoute() is the first hook that vite-plugin-ssr calls, which means that the rest of our app doesn't have to deal with localized URLs anymore and we can simply use pageContext.locale instead.

See Guides > Access pageContext anywhere for being able to access pageContext.locale in any React/Vue component.

This technique also works with:

  • ?lang=fr query parameters

  • domain.fr domain TLDs

  • Accept-Language: fr-FR headers

    The Accept-Language header can be used for redirecting the user to the right localized URL (e.g. URL /about + Header Accept-Language: de-DE => redirect to /de-DE/about). Once the user is redirected to a localized URL, we can use the technique described above. We can perform the redirection by using our server (e.g. Express.js) independently of vite-plugin-ssr.

    Using the Accept-Language header to show different languages for the same URL is considered bad practice for both SEO and UX reasons. It's recommended to use Accept-Language only to redirect the user.

Examples

Pre-rendering

If we use pre-rendering then, in addition to defining onBeforeRoute(), we also define a second hook onBeforePrerender():

// _default.page.server.js

export { onBeforePrerender }

const locales = ['en-US', 'de-DE', 'fr-FR']
const localeDefault = 'en-US'

function onBeforePrerender(prerenderContext) {
  const pageContexts = []
  prerenderContext.pageContexts.forEach((pageContext) => {
    // Duplicate pageContext for each locale
    locales.forEach((locale) => {
      // Localize URL
      let { urlOriginal } = pageContext
      if (locale !== localeDefault) {
        urlOriginal = `/${locale}${pageContext.urlOriginal}`
      }
      pageContexts.push({
        ...pageContext,
        urlOriginal,
        // Set pageContext.locale
        locale
      })
    })
  })
  return {
    prerenderContext: {
      pageContexts
    }
  }
}

See /examples/i18n/ for an example using onBeforePrerender().

Our prerender() hooks (if we use any) return URLs without any locale (e.g. prerender() returning /product/42). Instead, it's our onBeforePrerender() hook that duplicates and modifies URLs for each locale (e.g. duplicating /product/42 into /en-US/product/42, /de-DE/product/42, /fr-FR/product/42).

// /pages/product.page.server.js

export { prerender }

async function prerender() {
  const products = await Product.findAll()
  const URLs = products.map(({ id }) => '/product/' + id)
  // We don't add the locale here, instead we let onBeforePrerender() add the locales
  return URLs
}

We basically use prerender() to determine URLs and/or load data, and use onBeforePrerender() merely to manipulate the URLs and set pageContext.locale.

Despite what the naming suggests, note that vite-plugin-ssr calls our onBeforePrerender() hook after our prerender() hooks. Also note that onBeforePrerender(prerenderContext) is a global hook we can define only once, while prerender() (it doesn't have any argument) is a per-page hook we can define multiple times.

Alternatively, if we need to load data that depends on localization, instead of onBeforePrerender() we can use prerender() to localize pageContext:

// /pages/product.page.server.js

// In this example, we don't use onBeforePrerender() but, instead,
// we use prerender() to duplicate and localize URLs and their pageContext
export { prerender }

async function prerender() {
  // Load data
  const products = await Product.findAll()

  // Set pageContext + localize
  const urlsWithPageContext = []
  products.forEach(product => {
    ['en-US', 'de-DE', 'fr-FR'].forEach(locale => {
      urlsWithPageContext.push({
        url: `/${locale}/product/${product.id}`,
        pageContext: {
          locale,
          product,
          pageProps: {
            product: {
              name: product.name,
              description: product.description,
              price: product.price,
              // ...
            }
          }
        }
      })
    })
  })

  return urlsWithPageContext
}

We may still need to use onBeforePrerender() for localizing static pages that don't load data:

// _default.page.server.js

export { onBeforePrerender }

import assert from 'assert'

const locales = ['en-US', 'de-DE', 'fr-FR']

function onBeforePrerender(prerenderContext) {
  const pageContexts = []
  prerenderContext.pageContexts.forEach((pageContext) => {
    if(pageContext.locale) {
      // Already localized by one of our prerender() hooks
      assert(pageContext.urlOriginal.startsWith(`/${pagecontext.locale}/`))
      pageContexts.push(pageContext)
    } else {
      // Duplicate pageContext for each locale
      locales.forEach((locale) => {
        // Localize URL and pageContext
        pageContexts.push({
          ...pageContext,
          urlOriginal: `/${locale}${pageContext.urlOriginal}`,
          locale
        })
      })
    }
  })
  return {
    prerenderContext: {
      pageContexts
    }
  }
}