Files
theater-ziefen-website/.agents/skills/sanity-best-practices/references/localization.md
T
johannes.gasser 57af0b8386
Build and Deploy / build-and-deploy (push) Successful in 2m53s
add skills
2026-05-18 08:39:42 +02:00

433 lines
11 KiB
Markdown

---
title: Sanity Localization Rules
description: Localization patterns for Sanity using official plugins and best practices.
---
# Sanity Localization Rules
Use the contents list to jump directly to the localization pattern you need.
## Table of Contents
- Guiding principles
- Terminology
- Locale content type
- Choosing document-level vs field-level localization
- Document-level localization
- Localized singletons
- Field-level localization
- AI-powered translation
- UI enhancement
- Frontend URL best practices
## 1. Guiding Principles
### Priority: Easy Authoring Experience
The structured nature of Sanity schemas and GROQ make it easy to parse localized content for your frontend. **Never** let frontend architecture dictate your localization approach — prioritize the editor experience.
### Avoid Content Duplication
Don't create nearly identical copies with slight differences (e.g., US vs British English). Use Portable Text marks and custom blocks to swap out words or sections as needed.
## 2. Terminology
| Term | Definition |
|------|------------|
| **Internationalization (i18n)** | Designing your frontend to support multiple languages |
| **Localization** | Adapting content for a specific language/region |
| **Language Tag** | Code like `en`, `en-US`, `zh-Hant-TW` (per IETF RFC 5646) |
| **Locale** | A language tag with region info (e.g., `en-US`) |
## 3. Create a Locale Content Type
**Best Practice:** Store locales in Sanity, not just in code. This allows sharing between Studio and frontend.
```typescript
// schemaTypes/locale.ts
import { TranslateIcon } from '@sanity/icons'
import { defineField, defineType } from 'sanity'
export const localeType = defineType({
name: 'locale',
icon: TranslateIcon,
type: 'document',
fields: [
defineField({ name: 'name', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'tag', type: 'string', description: 'IANA tag (en, en-US)', validation: (r) => r.required() }),
defineField({ name: 'fallback', type: 'reference', to: [{ type: 'locale' }] }),
defineField({ name: 'default', type: 'boolean' }),
],
preview: { select: { title: 'name', subtitle: 'tag' } },
})
```
**Tip:** Restrict locale editing to admins via Structure by filtering `locale` from non-admin users.
## 4. Choose Your Localization Method
| Content Type | Examples | Recommended Method |
|--------------|----------|-------------------|
| **Structured** (things) | Products, People, Locations, Categories | Field-level |
| **Presentation** (UI) | Pages, Posts, Components | Document-level |
### Decision Questions
1. **Are fields shared across languages?** → Field-level
2. **Should changes be "global" for all locales?** (e.g., reordering components) → Field-level
3. **Is content mostly the same except regional differences?** → Field-level with PT marks
4. **Need to publish language versions independently?** → Document-level
## 5. Document-Level Localization
Use the **@sanity/document-internationalization** plugin.
```bash
npm install @sanity/document-internationalization
```
### Configuration
```typescript
// sanity.config.ts
import { documentInternationalization } from '@sanity/document-internationalization'
export default defineConfig({
plugins: [
documentInternationalization({
// Fetch from Content Lake
supportedLanguages: (client) =>
client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
// Document types to localize
schemaTypes: ['post', 'page'],
}),
],
})
```
### Add Language Field to Schema
```typescript
// In each schema type listed in schemaTypes
defineField({
name: 'language',
type: 'string',
readOnly: true,
hidden: true,
})
```
### Initial Value Templates
Pre-set language when creating documents outside the translation UI:
```typescript
// sanity.config.ts
import { template } from 'sanity'
export default defineConfig({
// ...
document: {
newDocumentOptions: (prev, { creationContext }) => {
// Filter to only show base language in "New document" menu
// The plugin handles creating translations from there
return prev.filter((item) =>
!['post', 'page'].includes(item.templateId) ||
item.parameters?.language === 'en'
)
},
},
// Initial value templates for each language
templates: (prev) => [
...prev,
template.initial({
id: 'post-en',
title: 'Post (English)',
schemaType: 'post',
parameters: [{name: 'language', type: 'string'}],
value: ({language}) => ({language}),
}),
],
})
```
### Querying Translated Documents
```groq
// Get document in specific language
*[_type == "post" && language == $locale && slug.current == $slug][0]
// Get all translations via metadata document
*[_type == "translation.metadata" && references($docId)][0] {
translations[] {
_key,
value-> { title, slug, language }
}
}
```
## 6. Localized Singletons (Homepage per Locale)
For singletons like homepages that need a separate document per locale, combine document-level localization with the singleton pattern.
### Schema Definition
```typescript
// schemaTypes/homePage.ts
import { HomeIcon } from '@sanity/icons'
import { defineType, defineField } from 'sanity'
export const homePageType = defineType({
name: 'homePage',
title: 'Home Page',
type: 'document',
icon: HomeIcon,
fields: [
defineField({
name: 'language',
type: 'string',
readOnly: true,
hidden: true,
}),
defineField({ name: 'title', type: 'string' }),
defineField({ name: 'pageBuilder', type: 'pageBuilder' }),
// ... other fields
],
preview: {
select: { language: 'language' },
prepare({ language }) {
return {
title: 'Home Page',
subtitle: language?.toUpperCase() || 'No language',
}
},
},
})
```
### Initial Value Templates
Create templates that pre-set the language for each locale:
```typescript
// sanity.config.ts
import { defineConfig, Template } from 'sanity'
// Define your supported locales
const LOCALES = [
{ id: 'en', title: 'English' },
{ id: 'fr', title: 'French' },
{ id: 'de', title: 'German' },
]
export default defineConfig({
// ...
templates: (prev) => {
// Create a template for each locale
const homePageTemplates: Template[] = LOCALES.map((locale) => ({
id: `homePage-${locale.id}`,
title: `Home Page (${locale.title})`,
schemaType: 'homePage',
parameters: [{ name: 'language', type: 'string' }],
value: { language: locale.id },
}))
return [...prev, ...homePageTemplates]
},
})
```
### Structure: Localized Singleton Helper
Create a helper to show one singleton per locale in the Structure:
```typescript
// src/structure/index.ts
import { StructureBuilder, StructureResolver } from 'sanity/structure'
import { HomeIcon } from '@sanity/icons'
const LOCALES = ['en', 'fr', 'de']
function createLocalizedSingleton(
S: StructureBuilder,
typeName: string,
title: string,
icon?: React.ComponentType
) {
return S.listItem()
.title(title)
.icon(icon)
.child(
S.list()
.title(title)
.items(
LOCALES.map((locale) =>
S.listItem()
.title(`${title} (${locale.toUpperCase()})`)
.icon(icon)
.child(
S.document()
.schemaType(typeName)
.documentId(`${typeName}-${locale}`) // Fixed ID per locale
.title(`${title} (${locale.toUpperCase()})`)
)
)
)
)
}
export const structure: StructureResolver = (S) =>
S.list()
.title('Content')
.items([
// Localized singletons
createLocalizedSingleton(S, 'homePage', 'Home Page', HomeIcon),
S.divider(),
// Filter localized singletons from default list
...S.documentTypeListItems().filter(
(item) => !['homePage'].includes(item.getId() as string)
),
])
```
### Querying Localized Singletons
```groq
// Get homepage for specific locale
*[_type == "homePage" && language == $locale][0]{
title,
pageBuilder[]{...}
}
// Or by fixed document ID
*[_id == "homePage-" + $locale][0]{...}
```
### Key Points
- **Fixed IDs:** Use `${typeName}-${locale}` pattern for predictable document IDs
- **Initial Value Templates:** Essential for the "New document" menu to work correctly
- **Structure:** Group all locale versions under one list item for cleaner navigation
- **See also:** `studio-structure.md` for more singleton patterns
## 7. Field-Level Localization
Use **sanity-plugin-internationalized-array** (NOT localized objects — they hit attribute limits).
```bash
npm install sanity-plugin-internationalized-array
```
### Configuration
```typescript
// sanity.config.ts
import { internationalizedArray } from 'sanity-plugin-internationalized-array'
export default defineConfig({
plugins: [
internationalizedArray({
languages: (client) =>
client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
fieldTypes: ['string', 'text', 'simpleBlockContent'],
}),
],
})
```
### Usage in Schema
```typescript
// The plugin creates types like `internationalizedArrayString`
defineField({
name: 'jobTitle',
type: 'internationalizedArrayString', // Localized string field
})
```
### Portable Text Localization
Create a reusable block content type, then add it to `fieldTypes`:
```typescript
// schemaTypes/simpleBlockContent.ts
export default defineType({
name: 'simpleBlockContent',
type: 'array',
of: [
{
type: 'block',
styles: [{ title: 'Normal', value: 'normal' }],
lists: [],
},
],
})
// sanity.config.ts
fieldTypes: ['string', 'simpleBlockContent']
// In your schema
defineField({
name: 'bio',
type: 'internationalizedArraySimpleBlockContent',
})
```
### Querying Internationalized Arrays
```groq
// Get specific locale value
*[_type == "author"][0] {
"jobTitle": jobTitle[_key == $locale][0].value
}
// With fallback
*[_type == "author"][0] {
"jobTitle": coalesce(
jobTitle[_key == $locale][0].value,
jobTitle[_key == "en"][0].value
)
}
```
## 8. AI-Powered Translation
Use **@sanity/assist** for automated translations.
```bash
npm install @sanity/assist
```
```typescript
// sanity.config.ts
import { assist } from '@sanity/assist'
export default defineConfig({
plugins: [
assist({
translate: {
// For document-level localization
document: {
languageField: 'language',
},
// For field-level localization
field: {
languages: (client) =>
client.fetch(`*[_type == "locale"]{ "id": tag, "title": name }`),
documentTypes: ['author', 'category'],
},
},
}),
],
})
```
## 9. UI Enhancement
Use **@sanity/language-filter** to let editors show/hide locales:
```bash
npm install @sanity/language-filter
```
## 10. Frontend URL Best Practices
**Always include locale in the URL** for SEO:
- `yoursite.com/en/my-page``yoursite.com/fr/my-page`
- `yoursite.com/my-page` → redirects to default locale
**Avoid:** Having the default locale at root without prefix — causes SEO edge cases.
Use Next.js middleware (or framework equivalent) to redirect paths missing a locale prefix to the default locale.