Localization

Localization

The toolkit supports as many locales as needed, including a single one.

A locale is a combination of a language and a location. For example, en-US is the locale for English in the United States, while en-GB is the locale for English in the United Kingdom.

If not differentiating by country, you can use the language code only, e.g. es for Spanish.

How it works

Refer to the high-level view of localization for the big picture. Below is a more detailed explanation of how the toolkit implements localization.

Configure locales in config.ts

Add the locales you want to support in the config.ts file, in the locales array. Each locale is of the shape:

interface LocaleConfiguration {
  /**
   * If adding full locales (English, USA) instead of just plain languages (English), they should
   * be formatted according to RFC 5646: Tags for Identifying Languages (also known as BCP 47).
   *
   * Example: `en-us` instead of `en_us`.
   *
   * Capitalized or not, it doesn't make a difference - we'll make them all lowercase.
   */
  value: string
  title: string
  icon: string
  isDefault?: boolean
}
 
// Example:
const locales: LocaleConfiguration[] = [
  {
    value: 'en',
    title: 'English',
    icon: '🇬🇧',
    isDefault: true,
  },
  {
    value: 'fr',
    title: 'French',
    icon: '🇫🇷',
  },
  {
    value: 'es',
    title: 'Spanish',
    icon: '🇪🇸',
  },
]

Configure localized types

Document types can be configured to be localized by adding the custom.localized property to the document type definition. Here's an example:

export const myDocumentType = documentSchema({
  name: 'myDocumentType',
  type: 'document',
  custom: {
    localized: true,
    // etc...
  },
  // ...
)}

With this, the documentSchema function (present in app/sanity/schemas/documentSchema.ts) will include the fields to achieve the data structure below.

The data structure for localized documents

Individual localized documents include:

  • locale: a string field with the value of the document's locale
  • translations: a reference to the translations meta document (see below)

The translations meta document exists only to hold references to the translations for a given document. It includes a single locales property, an array of references to each locale's document, where the _key of each item is the value of the locale (en, es, pt-br, etc.)

Fetching locale alternatives for current document

From the data structure above, we can fetch translations for the current document with the following GROQ approach:

app/queries/route.queries.ts
const EXAMPLE_ROUTE_QUERY = /* groq */ `
"routeData": *[${DOC_ID_FILTER}][0] {
  ...,
  // Follows the translations reference (meta document) and format each locale
  "translations": translations->.locales[] {
    "locale": _key,
    "internalLink": @->{ ${DOC_FOR_PATH_FRAGMENT} },
  },
}`

Linking to localized alternatives of the current route

The translations property fetched above will be an array of objects ready to be used to construct links. Here's how it's used in app/components/SEOHead.tsx to generate hreflang links:

export const SEOHead = (props) => {
  // ...
  return (
    <>
      {/* ... simplified for clarity */}
      {routeData.translations.map((translation) => (
        <link
          key={translation.locale}
          rel="alternate"
          hrefLang={
            translation.locale === config.defaultLocale.value
              ? 'x-default'
              : translation.locale
          }
          href={pathToAbsUrl(getDocumentPath(translation.internalLink))}
        />
      ))}
      {/* ... */}
    </>
  )
}

Filtering documents by locale in the front-end

When fetching data for routes in routeLoader, we ensure that both routePath and locale are matched to determine a valid document for the request. This starts by checking the locale of the request:

app/routing/routeLoader.ts
function parsePath(context: LoaderArgs) {
  const url = new URL(context.request.url)
  let routePath = stripMarginSlashes(url.pathname)
 
  // Assume default locale
  let locale = config.defaultLocale.value
 
  // But check if the first path segment is a valid locale
  if (isValidLocale(routePath.split('/')[0])) {
    locale = routePath.split('/')[0]
    routePath = routePath.split('/').slice(1).join('/')
  }
 
  return { url, routePath, locale }
}

Then, with the locale in hands, we pass that to getDocForRoute:

app/routing/routeLoader.ts
// simplified for clarity
async function getDocForRoute(url: URL, routePath: string, locale: string) {
  const docForRoute = await client.fetch(
    `*[
      locale == $requestLocale &&
      routePath.current in $routePaths
      // ...
    ][0]`,
    {
      // ...
      requestLocale: locale,
      routePaths: getPathVariations(routePath),
    },
  )
 
  // ...
}

The $requestLocale parameter will be available in GROQ queries, in case you need to do sub-queries in your routes' data resolvers.

For example, this is used in getCollectionFilterParts to only get documents of the current locale in a given collection.

The studio parts the toolkit handles

  • Localized items in the desk structure
  • Locale filters in the Pages Browser
  • Localized versions of new document options an initial value templates (opens in a new tab)
  • Document actions for duplicating localized documents
  • Translations view for creating/editing/resetting locales for the current document
  • Adding the localization fields to the schema of localized documents
  • Ensuring routePath is unique among all documents of the current locale, but allowing different locales to have the same routePath (e.g. /en/my-page and /es/my-page)