Sanity's Flexible Localization Approach
Sanity offers a flexible, plugin-driven approach to content localization that adapts to your content model rather than forcing a one-size-fits-all solution. Unlike traditional CMS platforms that impose rigid localization structures, Sanity empowers developers to choose between document-level and field-level translation strategies based on their specific use case, giving teams the flexibility to optimize for content editor workflows, publishing independence, or attribute efficiency.
This guide covers both localization approaches, the official plugins that simplify implementation, and practical patterns for querying and displaying localized content in your frontend applications. For teams just getting started with Sanity, we recommend reviewing the Sanity Getting Started guide to understand the fundamentals before diving into localization.
Understanding Localization Approaches in Sanity
Sanity provides two fundamental approaches to content localization. Understanding the differences between these approaches is essential for choosing the right strategy for your project.
Document-Level Translation
Document-level translation creates a separate document for each language version, connected through reference relationships. Each document has a language field value that identifies its locale, and a reference document serves as the hub that connects all translations together.
Key Characteristics:
- Each language has its own document in the Content Lake
- Reference documents connect translations into a translation set
- Enables independent publishing of each language version
- Best for content with significant structural differences per language
- Ideal when different teams manage different languages
Field-Level Translation
Field-level translation stores all language variants within a single document using either objects or arrays. This approach keeps translations together, making it easier to maintain consistency across languages.
Key Characteristics:
- All translations stored in one document
- Two schema patterns: objects (title.en, title.fr) vs arrays (title[].language, title[].value)
- Object pattern creates more unique attributes per language
- Array pattern scales better for many languages
- Simpler content management when fields need consistent translation
Sanity's official localization documentation provides comprehensive guidance on both approaches and their use cases.
| Aspect | Document-Level | Field-Level |
|---|---|---|
| Publishing | Independent per language | All languages together |
| Content Structure | Can vary per language | Consistent across languages |
| Team Workflow | Separate teams per language | Single team approach |
| Query Complexity | Reference expansion needed | Direct field access |
| Attribute Efficiency | High (per document) | Varies by schema pattern |
| Best For | Enterprise multilingual sites | Simple to medium localization |
Official Localization Plugins
Sanity provides official plugins that simplify implementing both localization approaches. These plugins handle the complex work of managing references, language fields, and Studio UI integration.
Document Internationalization Plugin
The @sanity/document-internationalization plugin automates document-level translation workflows. It creates reference relationships between translations and manages the language field across your documents.
Plugin Features:
- Automatic creation of translation documents
- Language field assignment and management
- Reference relationship handling between translations
- Integration with Sanity Studio desk tool
- Support for weak and strong references
Internationalized Array Plugin
The sanity-plugin-internationalized-array provides a custom UI for array-based field-level translations. This plugin makes it easy for editors to manage translations without complex popup dialogs.
Plugin Features:
- Custom inline editing interface
- Supports all field types (string, text, Portable Text)
- Scales efficiently for many languages
- Reduces unique attribute count
- Clean editor experience
Language Filter Plugin
The @sanity/language-filter plugin improves the Studio experience for editors working with multilingual content by allowing them to show or hide specific languages.
Plugin Features:
- Per-document-type language visibility control
- Reduces cognitive load in the Studio
- Integrates with other localization plugins
- Improves editor productivity
The document-internationalization plugin on GitHub provides detailed documentation on installation, configuration, and best practices for document-level localization workflows.
Choose the right plugin based on your localization approach and project requirements
document-internationalization
Official plugin for document-level translations. Creates reference relationships and manages language fields across separate documents.
internationalized-array
Custom UI plugin for array-based field translations. Supports any field type and scales efficiently across many languages.
language-filter
Studio UI plugin for filtering visible languages. Reduces editor complexity when working with multilingual content.
Schema Design for Localized Content
Designing effective schemas for multilingual content requires understanding the available patterns and choosing the right approach for your use case. Proper schema design is crucial for maintainable localization workflows, and the patterns shown here complement the approaches covered in our Sanity Schema Design guide.
Object-Based Field Localization
The object-based approach creates a separate field for each language within an object type. This pattern is straightforward but creates more unique attributes in your dataset.
Implementation Pattern:
// Define supported languages
const supportedLanguages = [
{ id: 'en', title: 'English', isDefault: true },
{ id: 'es', title: 'Spanish' },
{ id: 'fr', title: 'French' }
];
// Create locale string type
export const localeString = defineType({
title: 'Localized String',
name: 'localeString',
type: 'object',
fieldsets: [
{ title: 'Translations', name: 'translations', options: { collapsible: true } }
],
fields: supportedLanguages.map(lang => ({
title: lang.title,
name: lang.id,
type: 'string',
fieldset: lang.isDefault ? undefined : 'translations'
}))
});
Array-Based Field Localization
The array-based approach stores translations in an array of objects, each containing a language identifier and the translated value. This pattern is more attribute-efficient.
Implementation Pattern:
defineField({
name: 'title',
type: 'internationalizedArrayString',
languages: ['en', 'es', 'fr', 'de', 'it', 'pt']
});
Document-Level Configuration
For document-level localization, configure the document-internationalization plugin in your Sanity configuration:
import { documentInternationalization } from '@sanity/document-internationalization';
export default defineConfig({
plugins: [
deskTool(),
documentInternationalization({
schemaTypes: ['post', 'page', 'product'],
languageField: 'language',
referenceBehavior: 'weak',
defaultLanguages: ['en'],
withReferenceProblems: true
})
]
});
The Sanity localization documentation provides additional schema patterns and best practices for implementing localization in your project.
Querying Localized Content with GROQ
GROQ provides powerful patterns for querying localized content, including language selection, fallback strategies, and dynamic language handling. For a comprehensive guide to GROQ syntax and advanced querying patterns, see our Sanity GROQ Queries guide.
Basic Language Selection
Query specific language variants using direct property access for object-based localization:
// Fetch English title
*[_type == "presenter"][0] {
name,
"title": title.en
}
Fallback with Coalesce
Use the coalesce function to provide fallback values when a translation is missing:
// Fetch with language fallback
*[_type == "presenter"][0] {
"title": coalesce(title[$language], title.en, "Missing translation")
}
Dynamic Language Queries
Pass the language as a query parameter for flexible language selection:
// Query with dynamic language parameter
*[_type == "post" && slug.current == $slug][0] {
title,
"localizedTitle": coalesce(title[$language], title.en),
body
}
Filtering by Language
For document-level localization, filter and expand references by language:
// Get all translations of a document
*[_type == "post" && _id == $id][0] {
_id,
language,
title,
"translations": *[_type == "post" && references(^._id)] {
_id,
language,
title
}
}
The Sanity GROQ documentation includes additional query patterns and optimization strategies for multilingual content retrieval.
1// Query all posts with localized titles for a specific language2const localizedPostsQuery = `*[_type == "post"] | order(publishedAt desc) {3 _id,4 title,5 "localizedTitle": coalesce(title[$language], title.en),6 slug,7 publishedAt,8 "author": author->name9}`;10 11// Query single post with all translations12const postWithTranslationsQuery = `*[_type == "post" && slug.current == $slug][0] {13 _id,14 title,15 "localizedTitle": coalesce(title[$language], title.en),16 body,17 "localizedBody": body[$language],18 language,19 "allTranslations": *[_type == "post" && references(^._id)] {20 _id,21 language,22 title,23 slug24 }25}`;26 27// Query with dynamic language from request28const dynamicQuery = `*[_type == "product" && $language in supportedLanguages][0] {29 name,30 "description": coalesce(description[$language], description.en),31 "specifications": specifications[$language]32}`;Frontend Language Switcher Implementation
Implementing language switchers in frontend applications requires URL routing strategies and content adaptation patterns. For detailed integration patterns with Next.js, which is commonly used with Sanity, see our Sanity Next.js Integration guide.
URL-Based Language Routing
Structure your URLs to include language prefixes for SEO and user clarity:
/example-page (default language)
/es/ejemplo-pagina (Spanish)
/fr/page-exemple (French)
/de/beispielseite (German)
Next.js Implementation
Implement dynamic routing with locale parameters:
// app/[locale]/products/[slug]/page.tsx
import { client } from '@/lib/sanity';
const productQuery = `*[_type == "product" && slug.current == $slug][0] {
name,
"localizedName": coalesce(name[$locale], name.en),
description,
"localizedDescription": coalesce(description[$locale], description.en)
}`;
export default async function ProductPage({
params
}: {
params: { locale: string; slug: string }
}) {
const product = await client.fetch(productQuery, {
slug: params.slug,
locale: params.locale
});
return (
<article>
<h1>{product.localizedName}</h1>
<div>{product.localizedDescription}</div>
</article>
);
}
Language Switcher Component
Create a language switcher that preserves the current page:
// components/LanguageSwitcher.tsx
export default function LanguageSwitcher({ currentLocale, availableLocales }) {
return (
<select
value={currentLocale}
onChange={(e) => window.location.href = `/${e.target.value}${currentPath}`}
>
{availableLocales.map(locale => (
<option key={locale.code} value={locale.code}>
{locale.name}
</option>
))}
</select>
);
}
Best Practices and Decision Framework
Choosing the Right Approach
Use Document-Level Translation When:
- Different teams manage different languages independently
- Content structure varies significantly between languages
- Publishing schedules differ by language
- Content volume differs substantially between languages
- You need granular permission control per language
Use Field-Level Translation When:
- All languages publish simultaneously
- Content model is consistent across languages
- You prefer simpler content management
- You have a single team managing all languages
- Attribute efficiency is a concern for many languages
Performance Optimization
- Query Efficiency: Use projection to fetch only needed language fields
- Caching: Implement CDN caching per language variant
- Attribute Limits: Be mindful of Content Lake attribute limits when using object-based field localization
- GROQ Optimization: Use query parameters instead of string concatenation for language selection
Editor Experience
- Use the language filter plugin to reduce clutter in the Studio
- Organize fields with fieldsets to distinguish default from translated content
- Configure previews that show the correct language variant
- Provide documentation for content editors on localization workflows
Sanity's localization best practices provide additional guidance on optimizing your localization implementation for both performance and editor experience.
Summary
Sanity's localization system stands out for its flexibility and developer-friendly approach. By offering both document-level and field-level translation strategies through official plugins, teams can choose the implementation that best fits their content model and workflow requirements.
The document-internationalization plugin provides robust support for managing separate translation documents with reference relationships, ideal for enterprise multilingual sites. The internationalized-array plugin offers an efficient and editor-friendly approach for field-level translations, scaling well across many languages.
The GROQ query language provides powerful patterns for fetching localized content with intelligent fallback handling using the coalesce function. Combined with customizable URL routing and content adaptation strategies, Sanity enables building sophisticated multilingual experiences.
Whether building a simple bilingual site or a complex multilingual platform serving dozens of languages, Sanity provides the tools and patterns needed for professional-grade content localization.