Dynamic language loading in Sanity schemas not currently supported
The core issue you're facing is that Sanity schema definitions are synchronous and evaluated at build/startup time, but you're trying to use async GROQ queries to fetch data from documents to dynamically generate fields. Unfortunately, this pattern isn't supported in Sanity's schema system.
Here's why your current approach won't work:
fields: [
{
client.fetch(query, params).then(langs => { // ❌ Can't use async/promises in schema
langs.forEach(lang => ({
// ...
}))
})
}
]The fields array expects synchronous field definitions, not promises. Even if you tried to make the entire schema export async, Sanity wouldn't process it correctly.
Recommended Solutions
Based on the community discussion I found, here are your best options:
1. Define all possible language fields, use hidden to control visibility
Define all possible languages in your schema, then use the hidden property to conditionally show/hide them. The limitation is that hidden can only access the current document's data, not other documents via GROQ.
fields: allLanguages.map((lang) => ({
name: lang.name,
title: lang.title,
type: 'text',
fieldset: lang.isDefault ? null : 'translations',
hidden: ({document}) => {
// Can only check current document values
// Can't do async GROQ queries here
return !document?.selectedLanguages?.includes(lang.name)
}
}))2. Use the official @sanity/language-filter plugin (Recommended)
This plugin is designed exactly for your use case - managing localized content with configurable languages. It provides a UI to select which languages to show and handles the field visibility for you.
3. Use a custom input component
Instead of trying to generate fields dynamically, create a custom input component that fetches the language configuration and renders the appropriate inputs:
import {useEffect, useState} from 'react'
import {useClient} from 'sanity'
export default {
name: 'localeString',
type: 'object',
components: {
input: (props) => {
const client = useClient({apiVersion: '2023-01-01'})
const [languages, setLanguages] = useState([])
useEffect(() => {
client.fetch('*[_type == "lang"]{name, title, isDefault}')
.then(setLanguages)
}, [])
return (
<div>
{languages.map(lang => (
<div key={lang.name}>
<label>{lang.title}</label>
<input
value={props.value?.[lang.name] || ''}
onChange={(e) => props.onChange({
...props.value,
[lang.name]: e.target.value
})}
/>
</div>
))}
</div>
)
}
},
fields: [] // Empty, handled by custom component
}Check out the custom input widgets documentation for more details.
4. Store as a simple object with dynamic keys
Instead of predefined fields, store translations as a simple object:
{
name: 'translations',
type: 'object',
// No predefined fields - just a flexible object
}Then use a custom input component to manage the dynamic language keys based on your lang documents.
Bottom Line
The schema system is intentionally synchronous and static. For dynamic, content-driven field generation, you'll need to move that logic into custom input components or use existing solutions like the language-filter plugin. The custom component approach gives you full control to fetch your lang documents and render appropriate inputs at runtime.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.