Best approach for retrieving translations in GROQ queries from frontend?

8 replies
Last updated: Oct 4, 2021
Hey, is there a best approach for dealing with retrieving translations when running groq queries from the frontend?
Some context to this question
👇• We are using the
intl plugin for sanity to handle localization for our app. • We had to do some digging in order to retrieve blog articles data + associated translations. In our search,
this article has been sort of a guiding beacon.
WHAT WE'VE ATTEMPTED:• WHAT WORKS WELL
👍 The groq query highlighted in the above article works well when we use it in our sanity studio. The query's reference to
__i18n_lang && __i18n_refs
is represented by
_lang && _langRefs
for us as we've configured it so. So the query ran on studio looks something like this:
*[_type == "blog"]{
  _id,
  _lang,
  _langRefs,
  !(_id match "i18n*") => {
  "translations": *[_id in path("i18n." + ^._id + ".*")]
  },
  _id match "i18n*" => {
  "translations":
    *[^._id in _langRefs[].ref._ref]{_id, _lang} + 
    *[^._id in _langRefs[].ref._ref][0]{
      "matches": *[_id in path("i18n." + ^._id + ".*")]{_id, _lang}
    }.matches
  },

In our Sanity Studio, the results from this query return a blog article + an array of its
translations
filled with data of the translated blog articles.
• WHAT DOESN'T WORK SO WELL
👎 When attempting the same query in our frontend Next.js environment, the results return a blog article however this time with an empty array of
translations
----

I feel like there is something we've underthought or overlooked in our approach here. I'm happy for any guidance that will help me get on the right path. Thank you
🙏
AI Update

I can see you're hitting a frustrating issue where your translation query works in Studio but returns empty translations on the frontend. This is a common problem when working with document-level localization patterns, and the root cause is almost certainly related to API perspectives.

The Core Issue: Different Perspectives Between Studio and Frontend

Your Sanity Studio's Vision tool and your frontend Next.js app are likely querying different "views" of your content. The API version 2025-02-19 introduced a breaking change where the default perspective switched from raw to published. This means:

  • Studio Vision: Often uses the raw perspective by default, which shows all documents including drafts
  • Your frontend: If using a recent API version without explicitly setting a perspective, it defaults to published, which only shows published documents

Since your path("i18n." + ^._id + ".*") pattern is looking for documents with specific ID patterns, if those translation documents aren't published (they're only drafts), they won't appear in your frontend results.

Quick Fix: Set Your Perspective Explicitly

Try explicitly setting the perspective in your frontend Sanity client:

import { createClient } from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset',
  apiVersion: '2024-01-01', // or your current version
  perspective: 'previewDrafts', // Shows published + drafts, prioritizing drafts
  useCdn: false, // Disable CDN while debugging
})

The previewDrafts perspective will give you both published and draft documents, prioritizing drafts when both exist. You could also try perspective: 'raw' which shows all documents.

Verify Translation Document Publication Status

Your translation documents (those with i18n.* IDs) might exist only as drafts. To debug this, run a simple query from your frontend:

const result = await client.fetch('*[_id match "i18n.*"]')
console.log(result)

If this returns empty but works in Studio, you've confirmed it's a perspective/publication issue. Make sure your translation documents are actually published (not just saved as drafts). In Sanity, draft documents have a drafts. prefix in their _id, and they won't appear in queries using the published perspective.

Systematic Debugging Steps

  1. Verify document visibility: Run *[_id match "i18n.*"] from your frontend with different perspectives:

    • With perspective: 'published' (current default for newer API versions)
    • With perspective: 'previewDrafts' (published + drafts, with drafts prioritized)
    • With perspective: 'raw' (all documents)
  2. Check publication status in Studio: Go to your Studio and verify that your translation documents (the ones with i18n.* IDs) are actually published. Look for the "Publish" button - if it's available, the document is still a draft.

  3. Disable CDN caching: Set useCdn: false temporarily to ensure you're not hitting stale cached results.

  4. Compare Studio Vision configuration: Your Studio's Vision tool might be using a different perspective than your frontend. Check if there's a perspective selector in Vision.

  5. Test with simpler queries first: Before running the complex translation query, make sure you can see the basic documents with *[_type == "blog"] and the translation documents with *[_id match "i18n.*"].

About Your Query Pattern

The approach you're using with path("i18n." + ^._id + ".*") relies on document ID pattern matching, which is sensitive to perspective and publication state. The pattern itself is solid, but it requires that the documents it's looking for are visible in the perspective you're querying with.

For immediate debugging:

// In your frontend client configuration
const client = createClient({
  // ... your existing config
  perspective: 'previewDrafts',
  useCdn: false,
})

For production: Once you've confirmed the issue, decide on your approach:

  • Option A: Ensure all translation documents are properly published, then use perspective: 'published' in production
  • Option B: Use perspective: 'previewDrafts' if you need to show draft translations in your frontend
  • Option C: Adjust your content workflow to better match your querying needs

The key insight here is that Studio and your frontend are querying different views of your content due to perspective differences. Studio's Vision tool often shows all documents (similar to raw perspective), while your frontend client—especially if using a recent API version—defaults to only published documents. This explains why you see translations in Studio but not in your Next.js app.

Show original thread
8 replies
Are you fetching data with a token? Because of the id structure of the translations they are treated as private: https://www.sanity.io/docs/ids#fdc25ada5db2
I would like to think so but maybe we are not 🤷
So we initially configure our sanity client using
next-sanity
package. There is a
sanity.js
that takes into account all the configurations needed. In here is where we configure which dataset, project etc
When making the above mentionedgroq query, we're utilizing
groq
method from
next-sanity
:

const query = groq`*[_type == "blog"]{
  _id,
  _lang,
  _langRefs,
  !(_id match "i18n*") => {
  "translations": *[_id in path("i18n." + ^._id + ".*")]
  },
  _id match "i18n*" => {
  "translations":
    *[^._id in _langRefs[].ref._ref]{_id, _lang} + 
    *[^._id in _langRefs[].ref._ref][0]{
      "matches": *[_id in path("i18n." + ^._id + ".*")]{_id, _lang}
    }.matches
  }
`;
The calling of this query in
getStaticProps
is as follows:
  const [blogDataWithTranslations] = await getClient(preview).fetch(query);

--- > Do you mean there should be a token added to the fetch call?
Yes, because if you are fetching data without a token you not get the private documents. You can read more about that here: https://www.sanity.io/docs/keeping-your-data-safe
An alternative is to change the id structure of the translated docs, but that will make querying more difficult:
https://github.com/LiamMartens/sanity-plugin-intl-input/blob/develop/docs/general-configuration.md
I had this exact problem the first time I used the plugin
😉
Ooo wow 😮
So just so I'm clear on this:
1. I would need to generate an API token for my dataset in my
sanity.io/manage environment. 2. Use the token in the
getClient
call?
I'm assuming it passes as some sort of parameter in the fetcher-->
await getClient(preview).fetch(query, {ACCESS_TOKEN});
you include your token when creating clients via next-sanity, i.e

const client = createClient({
  projectId: ...,
  dataset: ...,
  token: 'token here'
})
I usually create a separate clients for client-side queries, without a token
That was it! Thank you both, this helped a lot 🙏
Sorry for the late reply, glad to hear that you got good help from others in the meantime Malcolm! 😄 🎉

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?