Internationalization (i18n)

We can internationalize (i18n) a Vike 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,
      // When pageContext.urlLogical is set, then Vike's router uses the value of
      // pageContext.urlLogical instead of pageContext.urlOriginal
      // In other words:
      //  - pageContext.urlOriginal => with locale, e.g. /en-US/movies
      //  - pageContext.urlLogical => without locale, e.g. /movies
      urlLogical: 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 Vike 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 Vike.

    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 Vike 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
    }
  }
}