From bbada9919948f22c48b0e4115012f971f1afbd6c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 13 May 2026 11:54:08 +0000 Subject: [PATCH] refactor: centralize survey language resolution --- apps/web/lib/survey/service.ts | 3 + apps/web/locales/en-US.json | 4 +- .../web/modules/survey/link/lib/utils.test.ts | 22 ++++ apps/web/modules/survey/link/lib/utils.ts | 76 ++---------- .../components/language-view.tsx | 11 +- docs/api-reference/openapi.json | 10 ++ .../multi-language-surveys.mdx | 41 +++++-- packages/js-core/package.json | 3 + .../src/lib/common/tests/utils.test.ts | 12 ++ packages/js-core/src/lib/common/utils.ts | 76 ++---------- packages/types/surveys/language.ts | 108 ++++++++++++++++++ pnpm-lock.yaml | 4 + 12 files changed, 224 insertions(+), 146 deletions(-) create mode 100644 packages/types/surveys/language.ts diff --git a/apps/web/lib/survey/service.ts b/apps/web/lib/survey/service.ts index e7844f0bbe..a7e029e3b4 100644 --- a/apps/web/lib/survey/service.ts +++ b/apps/web/lib/survey/service.ts @@ -592,9 +592,12 @@ export const createSurvey = async ( try { const { createdBy, languages, ...restSurveyBody } = parsedSurveyBody; const actionClasses = await getActionClasses(parsedEnvironmentId); + const hasMultipleEnabledLanguages = (languages ?? []).filter((language) => language.enabled).length > 1; let data: Omit = { ...restSurveyBody, + autoSelectLanguage: + restSurveyBody.autoSelectLanguage ?? (hasMultipleEnabledLanguages ? true : undefined), // @ts-expect-error - languages would be undefined in case of empty array languages: languages?.length ? languages : undefined, triggers: restSurveyBody.triggers diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 905ca501b0..7fe94dd404 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -1370,8 +1370,8 @@ "auto_save_disabled": "Auto-save disabled", "auto_save_disabled_tooltip": "Your survey is only auto-saved when in draft. This assures public surveys are not unintentionally updated.", "auto_save_on": "Auto-save on", - "auto_select_browser_language": "Auto-select browser language", - "auto_select_browser_language_description": "Use the respondent's browser language when the survey has a matching active language. Falls back to the default language.", + "auto_select_browser_language": "Use browser language by default", + "auto_select_browser_language_description": "Automatically open the survey in the respondent's browser language when that language is active. Falls back to the default language.", "automatically_close_survey_after_n_seconds_if_no_response": "Automatically close survey after seconds after trigger if no response.", "automatically_close_the_survey_after_a_certain_number_of_responses": "Automatically close the survey after a certain number of responses.", "automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds": "Automatically close the survey if the user does not respond after certain number of seconds.", diff --git a/apps/web/modules/survey/link/lib/utils.test.ts b/apps/web/modules/survey/link/lib/utils.test.ts index 2034631d08..1a0dd0816f 100644 --- a/apps/web/modules/survey/link/lib/utils.test.ts +++ b/apps/web/modules/survey/link/lib/utils.test.ts @@ -147,6 +147,28 @@ describe("getSurveyLanguageCode", () => { expect(getSurveyLanguageCode(undefined, survey, ["es-MX", "en-US"])).toBe("es-ES"); }); + test("uses aliases and ignores disabled languages", () => { + const survey = { + ...createMockSurvey([ + language("en", { default: true }), + language("de", { enabled: false }), + language("fr-FR", { + language: { + id: "lang-fr-FR", + code: "fr-FR", + alias: "fr", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "p1", + }, + }), + ]), + autoSelectLanguage: true, + }; + + expect(getSurveyLanguageCode(undefined, survey, ["de-DE", "fr-CA"])).toBe("fr-FR"); + }); + test("falls back to default language when auto-selection is disabled or unmatched", () => { const survey = createMockSurvey([language("en", { default: true }), language("de")]); diff --git a/apps/web/modules/survey/link/lib/utils.ts b/apps/web/modules/survey/link/lib/utils.ts index e6b0e34cfe..17971b4048 100644 --- a/apps/web/modules/survey/link/lib/utils.ts +++ b/apps/web/modules/survey/link/lib/utils.ts @@ -1,6 +1,7 @@ import { TJsEnvironmentStateSurvey } from "@formbricks/types/js"; import { TSurveyBlock } from "@formbricks/types/surveys/blocks"; import { TSurveyElement } from "@formbricks/types/surveys/elements"; +import { resolveSurveyLanguage } from "@formbricks/types/surveys/language"; import { TSurvey } from "@formbricks/types/surveys/types"; export function isRTL(text: string): boolean { @@ -55,77 +56,20 @@ export function isRTLLanguage(survey: TJsEnvironmentStateSurvey, languageCode: s export const getElementsFromSurveyBlocks = (blocks: TSurveyBlock[]): TSurveyElement[] => blocks.flatMap((block) => block.elements); -const normalizeLanguageCode = (languageCode: string): string => - languageCode.trim().split(";")[0].replace("_", "-").toLowerCase(); - -const getBaseLanguageCode = (languageCode: string): string => - normalizeLanguageCode(languageCode).split("-")[0]; - -const getSelectableLanguageCode = (surveyLanguage: TSurvey["languages"][number]): string | undefined => { - if (surveyLanguage.default) return "default"; - if (!surveyLanguage.enabled) return undefined; - return surveyLanguage.language.code; -}; - -const findExactLanguageMatch = (survey: TSurvey, languageCode: string): string | undefined => { - const normalizedLanguageCode = normalizeLanguageCode(languageCode); - - const selectedLanguage = survey.languages.find((surveyLanguage) => { - return ( - normalizeLanguageCode(surveyLanguage.language.code) === normalizedLanguageCode || - (surveyLanguage.language.alias - ? normalizeLanguageCode(surveyLanguage.language.alias) === normalizedLanguageCode - : false) - ); - }); - - return selectedLanguage ? getSelectableLanguageCode(selectedLanguage) : undefined; -}; - -const findLooseLanguageMatch = (survey: TSurvey, languageCode: string): string | undefined => { - const baseLanguageCode = getBaseLanguageCode(languageCode); - - for (const surveyLanguage of survey.languages) { - const selectableLanguageCode = getSelectableLanguageCode(surveyLanguage); - if (!selectableLanguageCode) continue; - - const languageBaseCode = getBaseLanguageCode(surveyLanguage.language.code); - const aliasBaseCode = surveyLanguage.language.alias - ? getBaseLanguageCode(surveyLanguage.language.alias) - : undefined; - - if (languageBaseCode === baseLanguageCode || aliasBaseCode === baseLanguageCode) { - return selectableLanguageCode; - } - } - - return undefined; -}; - export const getSurveyLanguageCode = ( langParam: string | undefined, survey: TSurvey, browserLanguageCodes: string[] = [] ): string => { - if (langParam) { - return findExactLanguageMatch(survey, langParam) ?? "default"; - } - - if (!survey.autoSelectLanguage) { - return "default"; - } - - for (const browserLanguageCode of browserLanguageCodes) { - const exactMatch = findExactLanguageMatch(survey, browserLanguageCode); - if (exactMatch) return exactMatch; - } - - for (const browserLanguageCode of browserLanguageCodes) { - const looseMatch = findLooseLanguageMatch(survey, browserLanguageCode); - if (looseMatch) return looseMatch; - } - - return "default"; + return ( + resolveSurveyLanguage({ + languages: survey.languages, + explicitLanguageCode: langParam, + browserLanguageCodes, + autoSelectLanguage: survey.autoSelectLanguage, + unmatchedExplicitLanguageBehavior: "fallback", + }) ?? "default" + ); }; /** diff --git a/apps/web/modules/survey/multi-language-surveys/components/language-view.tsx b/apps/web/modules/survey/multi-language-surveys/components/language-view.tsx index 7bec391e2b..df8f2d5f0b 100644 --- a/apps/web/modules/survey/multi-language-surveys/components/language-view.tsx +++ b/apps/web/modules/survey/multi-language-surveys/components/language-view.tsx @@ -122,7 +122,7 @@ export const LanguageView = ({ buttonText: t("environments.surveys.edit.remove_translations"), buttonVariant: "destructive", onConfirm: () => { - updateSurveyTranslations(localSurvey, []); + updateSurveyTranslations({ ...localSurvey, autoSelectLanguage: false }, []); setIsMultiLanguageActivated(false); setConfirmationModalInfo((prev) => ({ ...prev, open: false })); }, @@ -139,6 +139,7 @@ export const LanguageView = ({ const language = projectLanguages.find((lang) => lang.code === languageCode); if (!language) return; + const isNewMultiLanguageSurvey = localSurvey.languages.length === 0; let languageExists = false; const newLanguages = localSurvey.languages.map((lang) => { @@ -154,7 +155,11 @@ export const LanguageView = ({ } setConfirmationModalInfo((prev) => ({ ...prev, open: false })); - setLocalSurvey({ ...localSurvey, languages: newLanguages }); + setLocalSurvey({ + ...localSurvey, + languages: newLanguages, + autoSelectLanguage: isNewMultiLanguageSurvey ? true : localSurvey.autoSelectLanguage, + }); }; const handleToggleLanguage = (code: string) => { @@ -221,7 +226,7 @@ export const LanguageView = ({ buttonText: t("environments.surveys.edit.remove_translations"), buttonVariant: "destructive", onConfirm: () => { - updateSurveyTranslations(localSurvey, []); + updateSurveyTranslations({ ...localSurvey, autoSelectLanguage: false }, []); setIsMultiLanguageActivated(false); setConfirmationModalInfo((prev) => ({ ...prev, open: false })); }, diff --git a/docs/api-reference/openapi.json b/docs/api-reference/openapi.json index 22b7a8ea09..654d27dc41 100644 --- a/docs/api-reference/openapi.json +++ b/docs/api-reference/openapi.json @@ -754,6 +754,10 @@ "example": null, "type": "boolean" }, + "autoSelectLanguage": { + "example": null, + "type": "boolean" + }, "delay": { "example": 0, "type": "integer" @@ -6156,6 +6160,7 @@ { "autoClose": null, "autoComplete": null, + "autoSelectLanguage": null, "createdAt": "2024-08-05T11:08:27.042Z", "createdBy": "clfv1zvij0000ru0gunwpy43a", "delay": 0, @@ -6340,6 +6345,7 @@ "value": { "autoClose": null, "autoComplete": null, + "autoSelectLanguage": null, "createdBy": null, "delay": 0, "displayLimit": null, @@ -6495,6 +6501,7 @@ "data": { "autoClose": null, "autoComplete": null, + "autoSelectLanguage": null, "createdAt": "2024-08-05T11:08:27.042Z", "createdBy": "clfv1zvij0000ru0gunwpy43a", "delay": 0, @@ -6761,6 +6768,7 @@ "data": { "autoClose": null, "autoComplete": null, + "autoSelectLanguage": null, "createdAt": "2024-08-05T11:08:27.042Z", "createdBy": "clfv1zvij0000ru0gunwpy43a", "delay": 0, @@ -7014,6 +7022,7 @@ "data": { "autoClose": null, "autoComplete": null, + "autoSelectLanguage": null, "createdAt": "2024-08-05T11:08:27.042Z", "createdBy": "clfv1zvij0000ru0gunwpy43a", "delay": 0, @@ -7217,6 +7226,7 @@ "data": { "autoClose": null, "autoComplete": null, + "autoSelectLanguage": null, "createdAt": "2024-08-05T11:08:27.042Z", "createdBy": "clfv1zvij0000ru0gunwpy43a", "delay": 0, diff --git a/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx b/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx index 78828a5fdb..e6513e431a 100644 --- a/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx +++ b/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx @@ -37,12 +37,14 @@ How to deliver a specific language depends on the survey type (app or link surve ![Add Multiple Languages to your Project](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/add-languages.webp) You can come back to this page anytime to add more languages or remove existing ones. + Return to the dashboard to create a new survey or edit an existing one: ![Survey Overview](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/surveys-home.webp) + @@ -52,19 +54,26 @@ How to deliver a specific language depends on the survey type (app or link surve Choose a **Default Language** for your survey. + New multi-language surveys use the respondent's browser language by default when a matching active language is + available. You can change this anytime with the **Use browser language by default** toggle. This setting is + separate from **Show language switch**; you can automatically open the right language without showing respondents a + manual language menu. + Changing the default language will reset all the translations you have made for the survey. + Add the languages from the dropdown that you want to support in your survey: ![Enable Multi-language for a survey](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/add-language-in-survey.webp) + You can now see the survey in the selected language by clicking on the language dropdown in any of the questions. - + Now you can translate all survey content, including questions, options, and button placeholders, into the selected language. @@ -74,6 +83,17 @@ How to deliver a specific language depends on the survey type (app or link surve +## Language Selection Order + +Formbricks selects the survey language in this order: + +1. An explicit `lang` URL parameter for link surveys, or an explicit SDK/user language for app surveys. +2. The respondent's browser language when **Use browser language by default** is enabled. +3. The survey's default language. + +Language matching first checks the exact identifier or alias. If there is no exact match, Formbricks checks language +variants with the same base language, for example `es-MX` can match an active `es-ES` translation. + ## App Surveys Configuration @@ -91,8 +111,11 @@ How to deliver a specific language depends on the survey type (app or link surve If a user has a language assigned, a survey has multi-language activated and it is missing a translation in - the language of the user, the survey will not be displayed. + the language of the user, the survey will not be displayed. When no user language is assigned and **Use browser + language by default** is enabled, Formbricks uses the browser language and falls back to the survey default if + there is no match. + @@ -104,7 +127,9 @@ How to deliver a specific language depends on the survey type (app or link surve ## Link Surveys Configuration -For link surveys, the translation delivery is dependent on the `lang` URL parameter. +For link surveys, the `lang` URL parameter always takes priority. If no `lang` parameter is present and **Use browser +language by default** is enabled, Formbricks uses the respondent's browser language when a matching active language is +available. @@ -122,7 +147,10 @@ For link surveys, the translation delivery is dependent on the `lang` URL parame - German: [https://app.Formbricks.com/s/clptfos2i1pj516pvhxqyu3bn?lang=de](https://app.Formbricks.com/s/clptfos2i1pj516pvhxqyu3bn?lang=de) - Without the `lang` parameter, Formbricks will show the survey in the default language you have set. + Without the `lang` parameter, Formbricks will use the browser language when **Use browser language by default** is + enabled and a matching active language exists. Otherwise, it will show the survey in the default language you have + set. + @@ -150,14 +178,13 @@ Formbricks fully supports Right-to-Left (RTL) languages such as Arabic, Hebrew, Add an RTL language (like Arabic or Hebrew) in the **Survey Languages** settings - - Create translations for your survey content in the RTL language - +Create translations for your survey content in the RTL language The survey will automatically display in RTL format when that language is selected ![RTL Language Support](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/rtl-support.webp) + diff --git a/packages/js-core/package.json b/packages/js-core/package.json index 199f18b55b..05b73e8487 100644 --- a/packages/js-core/package.json +++ b/packages/js-core/package.json @@ -41,6 +41,9 @@ "test": "vitest run", "test:coverage": "vitest run --coverage" }, + "dependencies": { + "@formbricks/types": "workspace:*" + }, "author": "Formbricks ", "devDependencies": { "@formbricks/config-typescript": "workspace:*", diff --git a/packages/js-core/src/lib/common/tests/utils.test.ts b/packages/js-core/src/lib/common/tests/utils.test.ts index 2812e0a0a4..e232de7af2 100644 --- a/packages/js-core/src/lib/common/tests/utils.test.ts +++ b/packages/js-core/src/lib/common/tests/utils.test.ts @@ -506,6 +506,18 @@ describe("utils.ts", () => { expect(getLanguageCode(survey, undefined, ["de-DE", "en-US"])).toBe("de"); expect(getLanguageCode({ ...survey, autoSelectLanguage: false }, undefined, ["de-DE"])).toBe("default"); }); + + test("does not fall back to browser language when an explicit user language is unavailable", () => { + const survey = { + autoSelectLanguage: true, + languages: [ + { language: { code: "en" }, default: true, enabled: true }, + { language: { code: "de" }, default: false, enabled: true }, + ], + } as unknown as TEnvironmentStateSurvey; + + expect(getLanguageCode(survey, "fr", ["de-DE"])).toBeUndefined(); + }); }); // --------------------------------------------------------------------------------- diff --git a/packages/js-core/src/lib/common/utils.ts b/packages/js-core/src/lib/common/utils.ts index 578de5189f..7caec62df5 100644 --- a/packages/js-core/src/lib/common/utils.ts +++ b/packages/js-core/src/lib/common/utils.ts @@ -1,3 +1,4 @@ +import { resolveSurveyLanguage } from "@formbricks/types/surveys/language"; import { Logger } from "@/lib/common/logger"; import type { TEnvironmentState, @@ -176,79 +177,18 @@ export const getDefaultLanguageCode = (survey: TEnvironmentStateSurvey): string if (defaultSurveyLanguage) return defaultSurveyLanguage.language.code; }; -const normalizeLanguageCode = (languageCode: string): string => - languageCode.trim().split(";")[0].replace("_", "-").toLowerCase(); - -const getBaseLanguageCode = (languageCode: string): string => - normalizeLanguageCode(languageCode).split("-")[0]; - -const getSelectableLanguageCode = (surveyLanguage: TEnvironmentStateSurvey["languages"][number]) => { - if (surveyLanguage.default) { - return "default"; - } - if (!surveyLanguage.enabled) { - return undefined; - } - return surveyLanguage.language.code; -}; - -const findExactLanguageMatch = (survey: TEnvironmentStateSurvey, language: string): string | undefined => { - const normalizedLanguageCode = normalizeLanguageCode(language); - - const selectedLanguage = survey.languages.find((surveyLanguage) => { - return ( - normalizeLanguageCode(surveyLanguage.language.code) === normalizedLanguageCode || - (surveyLanguage.language.alias - ? normalizeLanguageCode(surveyLanguage.language.alias) === normalizedLanguageCode - : false) - ); - }); - - return selectedLanguage ? getSelectableLanguageCode(selectedLanguage) : undefined; -}; - -const findLooseLanguageMatch = (survey: TEnvironmentStateSurvey, language: string): string | undefined => { - const baseLanguageCode = getBaseLanguageCode(language); - - for (const surveyLanguage of survey.languages) { - const selectableLanguageCode = getSelectableLanguageCode(surveyLanguage); - if (!selectableLanguageCode) continue; - - const languageBaseCode = getBaseLanguageCode(surveyLanguage.language.code); - const aliasBaseCode = surveyLanguage.language.alias - ? getBaseLanguageCode(surveyLanguage.language.alias) - : undefined; - - if (languageBaseCode === baseLanguageCode || aliasBaseCode === baseLanguageCode) { - return selectableLanguageCode; - } - } - - return undefined; -}; - export const getLanguageCode = ( survey: TEnvironmentStateSurvey, language?: string, fallbackLanguages: string[] = [] ): string | undefined => { - if (language) { - return findExactLanguageMatch(survey, language) ?? findLooseLanguageMatch(survey, language); - } - - if (!survey.autoSelectLanguage) return "default"; - - for (const fallbackLanguage of fallbackLanguages) { - const exactMatch = findExactLanguageMatch(survey, fallbackLanguage); - if (exactMatch) return exactMatch; - } - - for (const fallbackLanguage of fallbackLanguages) { - const looseMatch = findLooseLanguageMatch(survey, fallbackLanguage); - if (looseMatch) return looseMatch; - } - - return "default"; + return resolveSurveyLanguage({ + languages: survey.languages, + explicitLanguageCode: language, + browserLanguageCodes: fallbackLanguages, + autoSelectLanguage: survey.autoSelectLanguage, + unmatchedExplicitLanguageBehavior: "undefined", + }); }; export const getSecureRandom = (): number => { diff --git a/packages/types/surveys/language.ts b/packages/types/surveys/language.ts new file mode 100644 index 0000000000..db934c54d4 --- /dev/null +++ b/packages/types/surveys/language.ts @@ -0,0 +1,108 @@ +type TSurveyLanguageLike = { + default?: boolean | null; + enabled?: boolean | null; + language: { + code: string; + alias?: string | null; + }; +}; + +interface ResolveSurveyLanguageInput { + languages: T[]; + explicitLanguageCode?: string; + browserLanguageCodes?: string[]; + autoSelectLanguage?: boolean | null; + unmatchedExplicitLanguageBehavior?: "fallback" | "undefined"; +} + +export const normalizeLanguageCode = (languageCode: string): string => + languageCode.trim().split(";")[0].trim().replace("_", "-").toLowerCase(); + +const getBaseLanguageCode = (languageCode: string): string => + normalizeLanguageCode(languageCode).split("-")[0]; + +const getSelectableLanguageCode = (surveyLanguage: TSurveyLanguageLike): string | undefined => { + if (surveyLanguage.default) return "default"; + if (!surveyLanguage.enabled) return undefined; + return surveyLanguage.language.code; +}; + +const findExactLanguageMatch = ( + languages: T[], + languageCode: string +): string | undefined => { + const normalizedLanguageCode = normalizeLanguageCode(languageCode); + + const selectedLanguage = languages.find((surveyLanguage) => { + return ( + normalizeLanguageCode(surveyLanguage.language.code) === normalizedLanguageCode || + (surveyLanguage.language.alias + ? normalizeLanguageCode(surveyLanguage.language.alias) === normalizedLanguageCode + : false) + ); + }); + + return selectedLanguage ? getSelectableLanguageCode(selectedLanguage) : undefined; +}; + +const findLooseLanguageMatch = ( + languages: T[], + languageCode: string +): string | undefined => { + const baseLanguageCode = getBaseLanguageCode(languageCode); + + for (const surveyLanguage of languages) { + const selectableLanguageCode = getSelectableLanguageCode(surveyLanguage); + if (!selectableLanguageCode) continue; + + const languageBaseCode = getBaseLanguageCode(surveyLanguage.language.code); + const aliasBaseCode = surveyLanguage.language.alias + ? getBaseLanguageCode(surveyLanguage.language.alias) + : undefined; + + if (languageBaseCode === baseLanguageCode || aliasBaseCode === baseLanguageCode) { + return selectableLanguageCode; + } + } + + return undefined; +}; + +export const matchSurveyLanguage = ( + languages: T[], + languageCode: string +): string | undefined => { + return findExactLanguageMatch(languages, languageCode) ?? findLooseLanguageMatch(languages, languageCode); +}; + +/** + * Resolves survey language precedence without coupling callers to a delivery channel: + * explicit language (URL or SDK/user setting) -> browser languages when enabled -> survey default. + */ +export const resolveSurveyLanguage = ({ + languages, + explicitLanguageCode, + browserLanguageCodes = [], + autoSelectLanguage, + unmatchedExplicitLanguageBehavior = "fallback", +}: ResolveSurveyLanguageInput): string | undefined => { + if (explicitLanguageCode) { + const explicitMatch = matchSurveyLanguage(languages, explicitLanguageCode); + if (explicitMatch) return explicitMatch; + return unmatchedExplicitLanguageBehavior === "undefined" ? undefined : "default"; + } + + if (!autoSelectLanguage) return "default"; + + for (const browserLanguageCode of browserLanguageCodes) { + const exactMatch = findExactLanguageMatch(languages, browserLanguageCode); + if (exactMatch) return exactMatch; + } + + for (const browserLanguageCode of browserLanguageCodes) { + const looseMatch = findLooseLanguageMatch(languages, browserLanguageCode); + if (looseMatch) return looseMatch; + } + + return "default"; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f134040f9..14a8d1f6dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -756,6 +756,10 @@ importers: version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.4.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@29.0.2(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) packages/js-core: + dependencies: + '@formbricks/types': + specifier: workspace:* + version: link:../types devDependencies: '@formbricks/config-typescript': specifier: workspace:*