Open oliviercperrier opened 5 months ago
I have the same issue, this could be very useful for field level translations !
Similar to this, the following query generates isCurrentLanguage: unknown
:
"toasts": toasts[]->{"isCurrentLanguage": language == $language, code, title, content},
While if I use a string literal instead, the type is correctly generated as isCurrentLanguage: string
:
"toasts": toasts[]->{"isCurrentLanguage": language == "en", code, title, content},
I also noticed while debugging this query that when using the parent accessor, the type results in Array<never>
:
"toasts": *[_type == "toast" && language == $language && _id in ^.toasts[]{_ref}._ref]{code, heading, content, language}
In all these cases queries correctly work and bring the expected results.
Hi! This is is a tricky feature because of the dynamic nature of GROQ. Given the look up attribute[$param]
it's impossible to parse the query correctly without knowing the type of $param. If $param is a string it's an attribute access(object), if it's a number it's an element access(array), and by default it's a filter. Since we rely on the GROQ javascript parser we would need to know the value of $param while parsing, which we don't at the moment
One workaround I tried was to take my initial query with $lang
parameters and create a new "localized query" by replacing all references to [$lang]
with .en
. I thought this would work and then I'd be able to use the type generated for the localized query in the code.
import groq from 'groq';
const fullWidthTextBlockFragment = `
_type == 'module.fullWidthTextBlock' => {
_key,
_type,
"title": title[$lang],
"body": body[$lang],
}
`;
const SanityProductModulesQuery = groq`
*[_type == 'product' && store.slug.current == $handle][0] {
modules[] {
${fullWidthTextBlockFragment},
}
}`;
const LocalizedProductModulesQuery = groq`
${SanityProductModulesQuery.replaceAll('[$lang]', '.en')}
`;
⚠️ However it seems the Groq parser does not (yet?) allow those kind of expressions :
✖ Unsupported expression type: MemberExpression in ../app/groq/product.localized.ts:22:4 ⚠ Encountered errors in 1 files while generating types
groq(`
*[_type == 'product' && store.slug.current == $handle][0] {
_type == 'module.fullWidthTextBlock' => {
"title": title[$lang],
"body": body[$lang],
}
}`,
{
lang: 'en',
},
);
body : { fr: string; en: string }
I think it would be safe to assume that body[$something]
returns a string (or null)It would be nice being able to at least define parameters in JSDoc (or similar) format for this feature work:
/**
* @param language {string}
*/
const someQuery = groq`
*[_type == 'someType' && language == $language] {...}
`;
This approach might not be enough to unblock the usage of typed fragments though, which would be ideal.
As a workaround, you could try to use select
instead of body[$locale]
export const testQuery = groq`
*[_type == "localizedDocument"][0] {
"body": select(
$locale == "en" => body.en,
$locale == "fr" => body.fr,
)
}
`;
The resulting type of body
is as you expected:
export type TestQueryResult = {
body: string | null;
} | null;
However, this is inconvenient, because you always need to specify this selection. You could create a simple utility that reduces the burden of writing it. However, do not map over your supportedLanguges
to generate these selections. I've found that TypeGen throws an error when you do: ✖ Unsupported expression type: MemberExpression in sanity/schemas/documents/test.ts:48:6 in "sanity/schemas/documents/test.ts"
export const selectLocalizedString = (field: string) =>
`select(
$locale == "en" => ${field}.en,
$locale == "fr" => ${field}.fr,
)`;
// ⚠ Does not work with TypeGen
// export const selectLocalizedString = (field: string) =>
// `select(
// ${supportedLanguages.map((lang) => `$locale == "${lang}" => ${field}.${lang},`).join("\n")}
// )`;
export const testQuery = groq`
*[_type == "localizedDocument"][0] {
"body": ${selectLocalizedString("body")}
}
`;
Here's the full final file I used to reproduce your issue and find a working solution:
import { groq } from "next-sanity";
import { defineField, defineType } from "sanity";
const supportedLanguages = ["en", "fr"] as const;
export const localizedString = defineType({
title: "Localized string",
name: "localizedString",
type: "object",
fields: supportedLanguages.map((lang) => ({
title: lang,
name: lang,
type: "string",
})),
});
export default defineType({
name: "localizedDocument",
title: "Localized Document",
type: "document",
fields: [
defineField({
name: "body",
title: "Body",
type: localizedString.name,
validation: (rule) => rule.required(),
}),
],
});
// export const testQuery = groq`
// *[_type == "localizedDocument"][0] {
// "body": select(
// $locale == "en" => body.en,
// $locale == "fr" => body.fr,
// )
// }
// `;
export const selectLocalizedString = (field: string) =>
`select(
$locale == "en" => ${field}.en,
$locale == "fr" => ${field}.fr,
)`;
// ⚠ Does not work with TypeGen
// export const selectLocalizedString = (field: string) =>
// `select(
// ${supportedLanguages.map((lang) => `$locale == "${lang}" => ${field}.${lang},`).join("\n")}
// )`;
export const testQuery = groq`
*[_type == "localizedDocument"][0] {
"body": ${selectLocalizedString("body")}
}
`;
@maxijonson solutions works, however it seems that if you import selectLocalizedString
from another file, the TypeGen parser doesn't handle it properly :
✖ Could not find binding for node "field" in /.../common.ts:3:23
We are therefore constrained to redefine this function in all the files where we want to use it
Another workaround if you want to keep the coalesce function in the query until it's fixed;
An example query with ["$lang"] so type gen works as expected and returns a string for the coalesced title:
export const exampleQuery = groq`
*[_type == "aContentType"][0] {
"title": coalesce(title["$lang"], title.en, "Missing translation"),
}
;`
Then for the query to actually return the localised content and not always fallback to .en, create a small util function;
export const replaceDynamicLangParam = ({
query,
lang,
}: {
query: string;
lang: string;
}) => {
const placeholderPattern = /\["\$lang"\]/g;
const replacement = `["${lang}"]`;
return query.replace(placeholderPattern, replacement);
};
And then use the above in a sanityFetch util function;
"use server";
import { sanityClient } from "@/lib/sanity-clients";
import { revalidateTag } from "next/cache";
import { replaceDynamicLangParam } from "./utils/replace-dynamic-lang-param";
type OptionalParams = {
[key: string]: string;
};
export async function sanityFetch<T>({
query,
params = {},
cacheOptions = "default",
cacheTag = "sanity",
}: {
query: string;
params?: OptionalParams;
cacheOptions?:
| "no-cache"
| "no-store"
| "default"
| "reload"
| "force-cache"
| "only-if-cached";
cacheTag?: string;
}): Promise<T> {
// Workaround for typegen issue with dynamic params (with lang)
const queryToUse = replaceDynamicLangParam({
query: query,
lang: params?.lang || "en",
});
return await sanityClient.fetch(queryToUse, params, {
tag: cacheTag,
cache: cacheOptions,
});
}
And example usage to fetch data in a page/server component;
const data = await sanityFetch<ContactPageQueryResult>({
query: contactPageQuery,
params: {
lang: locale,
},
});
If you find a security vulnerability, do NOT open an issue. Email security@sanity.io instead.
Describe the bug
i have the following query:
*** This is working if i do directly access a locale: body.en
body is LocaleString which normally return
But i pass a locale to only get the currently selected lang in my website, so body is a string. But for some reason, typegen is still generatin this type:
To Reproduce
Steps to reproduce the behavior:
Expected behavior
I expect the typegen to generate the following type:
Which versions of Sanity are you using?
What operating system are you using?
Mac OS
Which versions of Node.js / npm are you running?
10.2.3 v18.19.0