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 thevalue
of the document's localetranslations
: 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:
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:
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
:
// 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 sameroutePath
(e.g./en/my-page
and/es/my-page
)