Dynamic localeString fields from Sanity document query

34 replies
Last updated: Nov 29, 2025
Hi there ☆I would like to change this `localeString.js`:

const supportedLanguages = [
  {
    name: 'fr',
    title: 'Français',
    isDefault: true, // important
  },
  {
    name: 'ja',
    title: '日本語',
  },
  {
    name: 'en',
    title: 'English',
  },
]

export default {
  name: 'localeString',
  title: 'LocaleString',
  type: 'object',
  fieldsets: [
    {
      title: 'Translations',
      name: 'translations',
    },
  ],
  fields: supportedLanguages.map(langx => (
    {
      name: langx.name,
      title: langx.title,
      type: 'text',
      fieldset: langx.isDefault ? null : 'translations',
    }
  )),
}
to make it point out instead of
const supportedLanguages
the data that are in a document called
lang.js
.this is `lang.js`:

export default {
    name: 'lang',
    title: 'Langue',
    type: 'document',
    fields: [
        {
            name: 'name',
            title: 'Name',
            type: 'string',
        },
        {
            name: 'title',
            title: 'Title',
            type: 'string',
        },
        {
            name: 'isDefault',
            title: 'IsDefault',
            type: 'boolean',
        },
    ],
}
In VISION I have this query:

*[_type == 'lang' ]{name, title, isDefault}
giving this result:

"result":[3 items
0:{3 items
"isDefault":false
"name":"ja"
"title":"日本語"
}
1:{3 items
"isDefault":true
"name":"fr"
"title":"Français"
}
2:{3 items
"isDefault":false
"name":"en"
"title":"English"
In the Docs the query example is:

const query = '*[_type == "bike" && seats >= $minSeats] {name, seats}'
const params = {minSeats: 2}

client.fetch(query, params).then(bikes => {
  console.log('Bikes with more than one seat:')
  bikes.forEach(bike => {
    console.log(`${bike.name} (${bike.seats} seats)`)
  })
})
So I modify
localeString.js
different ways but I am not able so far to find how to insert that into
fields
so it fits correctly in:
import client from 'part:@sanity/base/client'
import groq from "groq"

const query = groq `*[_type == 'lang']{
  name,
  title,
  isDefault
}`
const params = {}

export default {
  name: 'localeString',
  title: 'LocaleString',
  type: 'object',
  fieldsets: [
    {
      title: 'Translations',
      name: 'translations',
    },
  ],
  fields: [
    {
      client.fetch(query, params).then(langs => {
      langs.forEach(lang => ({
        name: lang.name,
        title: lang.title,
        type: 'text',
        fieldset: lang.isDefault ? null : 'translations',
      }))
      })
    }
  ]
}
I know that I miss the keys and everything with fields but I really don't know how I should do...
AI Update

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.

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)
  }
}))

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.

Show original thread
34 replies

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.

Was this answer helpful?