mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-19 03:04:39 -05:00
refactor: centralize survey language resolution
This commit is contained in:
@@ -631,6 +631,7 @@ export const createSurvey = async (workspaceId: string, surveyBody: TSurveyCreat
|
||||
const { createdBy, languages, ...restSurveyBody } = parsedSurveyBody;
|
||||
const normalizedCloseOn = restSurveyBody.closeOn instanceof Date ? restSurveyBody.closeOn : null;
|
||||
const normalizedPublishOn = restSurveyBody.publishOn instanceof Date ? restSurveyBody.publishOn : null;
|
||||
const hasMultipleEnabledLanguages = (languages ?? []).filter((language) => language.enabled).length > 1;
|
||||
|
||||
const actionClasses = await getActionClasses(parsedWorkspaceId);
|
||||
|
||||
@@ -641,6 +642,8 @@ export const createSurvey = async (workspaceId: string, surveyBody: TSurveyCreat
|
||||
publishOn: normalizedPublishOn,
|
||||
status: restSurveyBody.status ?? "draft",
|
||||
}),
|
||||
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
|
||||
|
||||
@@ -2833,8 +2833,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": "Automatically close survey after",
|
||||
"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.",
|
||||
|
||||
@@ -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")]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TJsWorkspaceStateSurvey } 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: TJsWorkspaceStateSurvey, languageCode: str
|
||||
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"
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -149,7 +149,7 @@ export const LanguageView = ({
|
||||
buttonVariant: "destructive",
|
||||
onConfirm: () => {
|
||||
// Strip all non-default language keys from the survey data
|
||||
let cleanedSurvey = localSurvey;
|
||||
let cleanedSurvey = { ...localSurvey, autoSelectLanguage: false };
|
||||
for (const lang of localSurvey.languages) {
|
||||
if (!lang.default) {
|
||||
cleanedSurvey = removeLanguageKeysFromSurvey(cleanedSurvey, lang.language.code);
|
||||
@@ -172,6 +172,7 @@ export const LanguageView = ({
|
||||
const language = workspaceLanguages.find((lang) => lang.code === languageCode);
|
||||
if (!language) return;
|
||||
|
||||
const isNewMultiLanguageSurvey = localSurvey.languages.length === 0;
|
||||
let languageExists = false;
|
||||
const newLanguages =
|
||||
localSurvey.languages.map((lang) => {
|
||||
@@ -187,7 +188,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) => {
|
||||
@@ -254,7 +259,7 @@ export const LanguageView = ({
|
||||
buttonText: t("workspace.surveys.edit.remove_translations"),
|
||||
buttonVariant: "destructive",
|
||||
onConfirm: () => {
|
||||
updateSurveyTranslations(localSurvey, []);
|
||||
updateSurveyTranslations({ ...localSurvey, autoSelectLanguage: false }, []);
|
||||
setIsMultiLanguageActivated(false);
|
||||
setConfirmationModalInfo((prev) => ({ ...prev, open: false }));
|
||||
},
|
||||
|
||||
@@ -328,6 +328,10 @@
|
||||
"example": null,
|
||||
"type": "boolean"
|
||||
},
|
||||
"autoSelectLanguage": {
|
||||
"example": null,
|
||||
"type": "boolean"
|
||||
},
|
||||
"delay": {
|
||||
"example": 0,
|
||||
"type": "integer"
|
||||
@@ -4367,6 +4371,7 @@
|
||||
{
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"autoSelectLanguage": null,
|
||||
"createdAt": "2024-08-05T11:08:27.042Z",
|
||||
"createdBy": "clfv1zvij0000ru0gunwpy43a",
|
||||
"delay": 0,
|
||||
@@ -4551,6 +4556,7 @@
|
||||
"value": {
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"autoSelectLanguage": null,
|
||||
"createdBy": null,
|
||||
"delay": 0,
|
||||
"displayLimit": null,
|
||||
@@ -4771,6 +4777,7 @@
|
||||
"data": {
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"autoSelectLanguage": null,
|
||||
"createdAt": "2024-08-05T11:08:27.042Z",
|
||||
"createdBy": "clfv1zvij0000ru0gunwpy43a",
|
||||
"delay": 0,
|
||||
@@ -5037,6 +5044,7 @@
|
||||
"data": {
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"autoSelectLanguage": null,
|
||||
"createdAt": "2024-08-05T11:08:27.042Z",
|
||||
"createdBy": "clfv1zvij0000ru0gunwpy43a",
|
||||
"delay": 0,
|
||||
@@ -5290,6 +5298,7 @@
|
||||
"data": {
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"autoSelectLanguage": null,
|
||||
"createdAt": "2024-08-05T11:08:27.042Z",
|
||||
"createdBy": "clfv1zvij0000ru0gunwpy43a",
|
||||
"delay": 0,
|
||||
@@ -5493,6 +5502,7 @@
|
||||
"data": {
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"autoSelectLanguage": null,
|
||||
"createdAt": "2024-08-05T11:08:27.042Z",
|
||||
"createdBy": "clfv1zvij0000ru0gunwpy43a",
|
||||
"delay": 0,
|
||||
|
||||
@@ -37,12 +37,14 @@ How to deliver a specific language depends on the survey type (app or link surve
|
||||

|
||||
|
||||
You can come back to this page anytime to add more languages or remove existing ones.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Create or Edit Your Survey">
|
||||
Return to the dashboard to create a new survey or edit an existing one:
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Enable Multi-language Support">
|
||||
@@ -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.
|
||||
|
||||
<Note>Changing the default language will reset all the translations you have made for the survey.</Note>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add Supported Languages">
|
||||
Add the languages from the dropdown that you want to support in your survey:
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Preview and Translate Content">
|
||||
|
||||
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.
|
||||
|
||||
</Step>
|
||||
@@ -74,6 +83,17 @@ How to deliver a specific language depends on the survey type (app or link surve
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 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
|
||||
|
||||
<Steps>
|
||||
@@ -91,8 +111,11 @@ How to deliver a specific language depends on the survey type (app or link surve
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Start Collecting Responses">
|
||||
@@ -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.
|
||||
|
||||
<Steps>
|
||||
<Step title="Publish Your Survey">
|
||||
@@ -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.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Start Collecting Responses">
|
||||
@@ -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
|
||||
</Step>
|
||||
|
||||
<Step title="Create Translations">
|
||||
Create translations for your survey content in the RTL language
|
||||
</Step>
|
||||
<Step title="Create Translations">Create translations for your survey content in the RTL language</Step>
|
||||
|
||||
<Step title="Automatic RTL Display">
|
||||
The survey will automatically display in RTL format when that language is selected
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/types": "workspace:*"
|
||||
},
|
||||
"author": "Formbricks <hola@formbricks.com>",
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
|
||||
@@ -456,6 +456,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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveSurveyLanguage } from "@formbricks/types/surveys/language";
|
||||
import { Logger } from "@/lib/common/logger";
|
||||
import type {
|
||||
TSurveyStyling,
|
||||
@@ -161,79 +162,18 @@ export const getDefaultLanguageCode = (survey: TWorkspaceStateSurvey): 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: TWorkspaceStateSurvey["languages"][number]) => {
|
||||
if (surveyLanguage.default) {
|
||||
return "default";
|
||||
}
|
||||
if (!surveyLanguage.enabled) {
|
||||
return undefined;
|
||||
}
|
||||
return surveyLanguage.language.code;
|
||||
};
|
||||
|
||||
const findExactLanguageMatch = (survey: TWorkspaceStateSurvey, 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: TWorkspaceStateSurvey, 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: TWorkspaceStateSurvey,
|
||||
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 => {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
type TSurveyLanguageLike = {
|
||||
default?: boolean | null;
|
||||
enabled?: boolean | null;
|
||||
language: {
|
||||
code: string;
|
||||
alias?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
interface ResolveSurveyLanguageInput<T extends TSurveyLanguageLike> {
|
||||
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 = <T extends TSurveyLanguageLike>(
|
||||
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 = <T extends TSurveyLanguageLike>(
|
||||
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 = <T extends TSurveyLanguageLike>(
|
||||
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 = <T extends TSurveyLanguageLike>({
|
||||
languages,
|
||||
explicitLanguageCode,
|
||||
browserLanguageCodes = [],
|
||||
autoSelectLanguage,
|
||||
unmatchedExplicitLanguageBehavior = "fallback",
|
||||
}: ResolveSurveyLanguageInput<T>): 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";
|
||||
};
|
||||
Generated
+4
@@ -840,6 +840,10 @@ importers:
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.4.0)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/js-core:
|
||||
dependencies:
|
||||
'@formbricks/types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
devDependencies:
|
||||
'@formbricks/config-typescript':
|
||||
specifier: workspace:*
|
||||
|
||||
Reference in New Issue
Block a user