Compare commits

...

9 Commits

Author SHA1 Message Date
Dhruwang Jariwala 74b679403d fix: multi-lang toggle covering arabic text (backport #7657) (#7660)
Co-authored-by: Johannes <johannes@formbricks.com>
2026-04-02 18:49:29 +05:30
Dhruwang Jariwala 4a5404557b fix: multilang button overflow (backport #7656) (#7659)
Co-authored-by: Niels Kaspers <kaspersniels@gmail.com>
2026-04-02 18:49:13 +05:30
Dhruwang Jariwala fbb529d066 fix: prevent language switch from breaking survey orientation and resetting language on auto-save (backport #7654) (#7658) 2026-04-02 18:48:57 +05:30
Dhruwang Jariwala e07b6fb3d1 fix: resolve language code case mismatch in link survey rendering (backport #7624) (#7625)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 17:35:18 +05:30
Dhruwang Jariwala 3996f89f75 fix: prevent auto-save from overwriting survey status during publish (backport) (#7622) 2026-03-30 16:31:39 +05:30
Dhruwang Jariwala 86f852ee4b fix: sync segment state after auto-save to prevent stale reference on publish (backport) (#7623)
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
2026-03-30 16:31:28 +05:30
Dhruwang Jariwala 5bf884d529 fix: handle 404 race condition in Stripe webhook reconciliation (backport #7584) (#7603) 2026-03-27 11:15:26 +05:30
Dhruwang Jariwala 351cd75e57 fix: prevent duplicate hobby subscriptions from race condition (backport #7597) (#7602)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 11:15:11 +05:30
Dhruwang Jariwala 2e36f0c590 fix: prevent multi-language survey buttons from falling back to English (backport #7559) (#7601)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 11:15:00 +05:30
14 changed files with 98 additions and 51 deletions
+3 -1
View File
@@ -84,7 +84,9 @@ export const extractLanguageIds = (languages: TLanguage[]): string[] => {
export const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: string | null) => { export const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: string | null) => {
if (!surveyLanguages?.length || !languageCode) return "default"; if (!surveyLanguages?.length || !languageCode) return "default";
const language = surveyLanguages.find((surveyLanguage) => surveyLanguage.language.code === languageCode); const language = surveyLanguages.find(
(surveyLanguage) => surveyLanguage.language.code.toLowerCase() === languageCode.toLowerCase()
);
return language?.default ? "default" : language?.language.code || "default"; return language?.default ? "default" : language?.language.code || "default";
}; };
+4 -10
View File
@@ -106,10 +106,7 @@ describe("billing actions", () => {
}); });
expect(mocks.getOrganization).toHaveBeenCalledWith("org_1"); expect(mocks.getOrganization).toHaveBeenCalledWith("org_1");
expect(mocks.ensureStripeCustomerForOrganization).toHaveBeenCalledWith("org_1"); expect(mocks.ensureStripeCustomerForOrganization).toHaveBeenCalledWith("org_1");
expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith( expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith("org_1");
"org_1",
"start-hobby"
);
expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1"); expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1");
expect(result).toEqual({ success: true }); expect(result).toEqual({ success: true });
}); });
@@ -128,10 +125,7 @@ describe("billing actions", () => {
} as any); } as any);
expect(mocks.ensureStripeCustomerForOrganization).not.toHaveBeenCalled(); expect(mocks.ensureStripeCustomerForOrganization).not.toHaveBeenCalled();
expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith( expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith("org_1");
"org_1",
"start-hobby"
);
expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1"); expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1");
expect(result).toEqual({ success: true }); expect(result).toEqual({ success: true });
}); });
@@ -145,7 +139,7 @@ describe("billing actions", () => {
expect(mocks.getOrganization).toHaveBeenCalledWith("org_1"); expect(mocks.getOrganization).toHaveBeenCalledWith("org_1");
expect(mocks.ensureStripeCustomerForOrganization).toHaveBeenCalledWith("org_1"); expect(mocks.ensureStripeCustomerForOrganization).toHaveBeenCalledWith("org_1");
expect(mocks.createProTrialSubscription).toHaveBeenCalledWith("org_1", "cus_1"); expect(mocks.createProTrialSubscription).toHaveBeenCalledWith("org_1", "cus_1");
expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith("org_1", "pro-trial"); expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith("org_1");
expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1"); expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1");
expect(result).toEqual({ success: true }); expect(result).toEqual({ success: true });
}); });
@@ -165,7 +159,7 @@ describe("billing actions", () => {
expect(mocks.ensureStripeCustomerForOrganization).not.toHaveBeenCalled(); expect(mocks.ensureStripeCustomerForOrganization).not.toHaveBeenCalled();
expect(mocks.createProTrialSubscription).toHaveBeenCalledWith("org_1", "cus_existing"); expect(mocks.createProTrialSubscription).toHaveBeenCalledWith("org_1", "cus_existing");
expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith("org_1", "pro-trial"); expect(mocks.reconcileCloudStripeSubscriptionsForOrganization).toHaveBeenCalledWith("org_1");
expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1"); expect(mocks.syncOrganizationBillingFromStripe).toHaveBeenCalledWith("org_1");
expect(result).toEqual({ success: true }); expect(result).toEqual({ success: true });
}); });
+2 -2
View File
@@ -216,7 +216,7 @@ export const startHobbyAction = authenticatedActionClient
throw new ResourceNotFoundError("OrganizationBilling", parsedInput.organizationId); throw new ResourceNotFoundError("OrganizationBilling", parsedInput.organizationId);
} }
await reconcileCloudStripeSubscriptionsForOrganization(parsedInput.organizationId, "start-hobby"); await reconcileCloudStripeSubscriptionsForOrganization(parsedInput.organizationId);
await syncOrganizationBillingFromStripe(parsedInput.organizationId); await syncOrganizationBillingFromStripe(parsedInput.organizationId);
return { success: true }; return { success: true };
}); });
@@ -248,7 +248,7 @@ export const startProTrialAction = authenticatedActionClient
} }
await createProTrialSubscription(parsedInput.organizationId, customerId); await createProTrialSubscription(parsedInput.organizationId, customerId);
await reconcileCloudStripeSubscriptionsForOrganization(parsedInput.organizationId, "pro-trial"); await reconcileCloudStripeSubscriptionsForOrganization(parsedInput.organizationId);
await syncOrganizationBillingFromStripe(parsedInput.organizationId); await syncOrganizationBillingFromStripe(parsedInput.organizationId);
return { success: true }; return { success: true };
}); });
@@ -150,7 +150,7 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin
await handleSetupCheckoutCompleted(event.data.object, stripe); await handleSetupCheckoutCompleted(event.data.object, stripe);
} }
await reconcileCloudStripeSubscriptionsForOrganization(organizationId, event.id); await reconcileCloudStripeSubscriptionsForOrganization(organizationId);
await syncOrganizationBillingFromStripe(organizationId, { await syncOrganizationBillingFromStripe(organizationId, {
id: event.id, id: event.id,
created: event.created, created: event.created,
@@ -1905,7 +1905,7 @@ describe("organization-billing", () => {
items: [{ price: "price_hobby_monthly", quantity: 1 }], items: [{ price: "price_hobby_monthly", quantity: 1 }],
metadata: { organizationId: "org_1" }, metadata: { organizationId: "org_1" },
}, },
{ idempotencyKey: "ensure-hobby-subscription-org_1-bootstrap" } { idempotencyKey: "ensure-hobby-subscription-org_1-0" }
); );
expect(mocks.prismaOrganizationBillingUpdate).toHaveBeenCalledWith({ expect(mocks.prismaOrganizationBillingUpdate).toHaveBeenCalledWith({
where: { organizationId: "org_1" }, where: { organizationId: "org_1" },
@@ -1974,7 +1974,7 @@ describe("organization-billing", () => {
], ],
}); });
await reconcileCloudStripeSubscriptionsForOrganization("org_1", "evt_123"); await reconcileCloudStripeSubscriptionsForOrganization("org_1");
expect(mocks.subscriptionsCancel).toHaveBeenCalledWith("sub_hobby", { prorate: false }); expect(mocks.subscriptionsCancel).toHaveBeenCalledWith("sub_hobby", { prorate: false });
expect(mocks.subscriptionsCreate).not.toHaveBeenCalled(); expect(mocks.subscriptionsCreate).not.toHaveBeenCalled();
@@ -458,18 +458,21 @@ const resolvePendingChangeEffectiveAt = (
const ensureHobbySubscription = async ( const ensureHobbySubscription = async (
organizationId: string, organizationId: string,
customerId: string, customerId: string,
idempotencySuffix: string subscriptionCount: number
): Promise<void> => { ): Promise<void> => {
if (!stripeClient) return; if (!stripeClient) return;
const hobbyItems = await getCatalogItemsForPlan("hobby", "monthly"); const hobbyItems = await getCatalogItemsForPlan("hobby", "monthly");
// Include subscriptionCount so the key is stable across concurrent calls (same
// count → same key → Stripe deduplicates) but changes after a cancellation
// (count increases → new key → allows legitimate re-creation).
await stripeClient.subscriptions.create( await stripeClient.subscriptions.create(
{ {
customer: customerId, customer: customerId,
items: hobbyItems, items: hobbyItems,
metadata: { organizationId }, metadata: { organizationId },
}, },
{ idempotencyKey: `ensure-hobby-subscription-${organizationId}-${idempotencySuffix}` } { idempotencyKey: `ensure-hobby-subscription-${organizationId}-${subscriptionCount}` }
); );
}; };
@@ -1264,8 +1267,7 @@ export const findOrganizationIdByStripeCustomerId = async (customerId: string):
}; };
export const reconcileCloudStripeSubscriptionsForOrganization = async ( export const reconcileCloudStripeSubscriptionsForOrganization = async (
organizationId: string, organizationId: string
idempotencySuffix = "reconcile"
): Promise<void> => { ): Promise<void> => {
const client = stripeClient; const client = stripeClient;
if (!IS_FORMBRICKS_CLOUD || !client) return; if (!IS_FORMBRICKS_CLOUD || !client) return;
@@ -1313,11 +1315,26 @@ export const reconcileCloudStripeSubscriptionsForOrganization = async (
); );
await Promise.all( await Promise.all(
hobbySubscriptions.map(({ subscription }) => hobbySubscriptions.map(async ({ subscription }) => {
client.subscriptions.cancel(subscription.id, { try {
prorate: false, await client.subscriptions.cancel(subscription.id, {
}) prorate: false,
) });
} catch (err) {
if (
err instanceof Stripe.errors.StripeInvalidRequestError &&
err.statusCode === 404 &&
err.code === "resource_missing"
) {
logger.warn(
{ subscriptionId: subscription.id, organizationId },
"Subscription already deleted, skipping cancel"
);
return;
}
throw err;
}
})
); );
return; return;
} }
@@ -1327,12 +1344,14 @@ export const reconcileCloudStripeSubscriptionsForOrganization = async (
// (e.g. webhook + bootstrap) both seeing 0 and creating duplicate hobbies. // (e.g. webhook + bootstrap) both seeing 0 and creating duplicate hobbies.
const freshSubscriptions = await client.subscriptions.list({ const freshSubscriptions = await client.subscriptions.list({
customer: customerId, customer: customerId,
status: "active", status: "all",
limit: 1, limit: 20,
}); });
if (freshSubscriptions.data.length === 0) { const freshActive = freshSubscriptions.data.filter((sub) => ACTIVE_SUBSCRIPTION_STATUSES.has(sub.status));
await ensureHobbySubscription(organizationId, customerId, idempotencySuffix);
if (freshActive.length === 0) {
await ensureHobbySubscription(organizationId, customerId, freshSubscriptions.data.length);
} }
} }
}; };
@@ -1340,6 +1359,6 @@ export const reconcileCloudStripeSubscriptionsForOrganization = async (
export const ensureCloudStripeSetupForOrganization = async (organizationId: string): Promise<void> => { export const ensureCloudStripeSetupForOrganization = async (organizationId: string): Promise<void> => {
if (!IS_FORMBRICKS_CLOUD || !stripeClient) return; if (!IS_FORMBRICKS_CLOUD || !stripeClient) return;
await ensureStripeCustomerForOrganization(organizationId); await ensureStripeCustomerForOrganization(organizationId);
await reconcileCloudStripeSubscriptionsForOrganization(organizationId, "bootstrap"); await reconcileCloudStripeSubscriptionsForOrganization(organizationId);
await syncOrganizationBillingFromStripe(organizationId); await syncOrganizationBillingFromStripe(organizationId);
}; };
@@ -346,8 +346,8 @@ export const MultipleChoiceElementForm = ({
</div> </div>
<div className="mt-2"> <div className="mt-2">
<div className="mt-2 flex items-center justify-between space-x-2"> <div className="mt-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex gap-2"> <div className="flex flex-wrap gap-2">
{specialChoices.map((specialChoice) => { {specialChoices.map((specialChoice) => {
if (element.choices.some((c) => c.id === specialChoice.id)) return null; if (element.choices.some((c) => c.id === specialChoice.id)) return null;
return ( return (
@@ -72,6 +72,7 @@ export const SurveyMenuBar = ({
const [lastAutoSaved, setLastAutoSaved] = useState<Date | null>(null); const [lastAutoSaved, setLastAutoSaved] = useState<Date | null>(null);
const isSuccessfullySavedRef = useRef(false); const isSuccessfullySavedRef = useRef(false);
const isAutoSavingRef = useRef(false); const isAutoSavingRef = useRef(false);
const isSurveyPublishingRef = useRef(false);
// Refs for interval-based auto-save (to access current values without re-creating interval) // Refs for interval-based auto-save (to access current values without re-creating interval)
const localSurveyRef = useRef(localSurvey); const localSurveyRef = useRef(localSurvey);
@@ -269,8 +270,8 @@ export const SurveyMenuBar = ({
// Skip if tab is not visible (no computation, no API calls for background tabs) // Skip if tab is not visible (no computation, no API calls for background tabs)
if (document.hidden) return; if (document.hidden) return;
// Skip if already saving (manual or auto) // Skip if already saving, publishing, or auto-saving
if (isAutoSavingRef.current || isSurveySavingRef.current) return; if (isAutoSavingRef.current || isSurveySavingRef.current || isSurveyPublishingRef.current) return;
// Check for changes using refs (avoids re-creating interval on every change) // Check for changes using refs (avoids re-creating interval on every change)
const { updatedAt: localUpdatedAt, ...localSurveyRest } = localSurveyRef.current; const { updatedAt: localUpdatedAt, ...localSurveyRest } = localSurveyRef.current;
@@ -289,10 +290,19 @@ export const SurveyMenuBar = ({
} as unknown as TSurveyDraft); } as unknown as TSurveyDraft);
if (updatedSurveyResponse?.data) { if (updatedSurveyResponse?.data) {
const savedData = updatedSurveyResponse.data;
// If the segment changed on the server (e.g., private segment was deleted when
// switching from app to link type), update localSurvey to prevent stale segment
// references when publishing
if (!isEqual(localSurveyRef.current.segment, savedData.segment)) {
setLocalSurvey({ ...localSurveyRef.current, segment: savedData.segment });
}
// Update surveyRef (not localSurvey state) to prevent re-renders during auto-save. // Update surveyRef (not localSurvey state) to prevent re-renders during auto-save.
// This keeps the UI stable while still tracking that changes have been saved. // This keeps the UI stable while still tracking that changes have been saved.
// The comparison uses refs, so this prevents unnecessary re-saves. // The comparison uses refs, so this prevents unnecessary re-saves.
surveyRef.current = { ...updatedSurveyResponse.data }; surveyRef.current = { ...savedData };
isSuccessfullySavedRef.current = true; isSuccessfullySavedRef.current = true;
setLastAutoSaved(new Date()); setLastAutoSaved(new Date());
} }
@@ -417,11 +427,13 @@ export const SurveyMenuBar = ({
}; };
const handleSurveyPublish = async () => { const handleSurveyPublish = async () => {
isSurveyPublishingRef.current = true;
setIsSurveyPublishing(true); setIsSurveyPublishing(true);
const isSurveyValidatedWithZod = validateSurveyWithZod(); const isSurveyValidatedWithZod = validateSurveyWithZod();
if (!isSurveyValidatedWithZod) { if (!isSurveyValidatedWithZod) {
isSurveyPublishingRef.current = false;
setIsSurveyPublishing(false); setIsSurveyPublishing(false);
return; return;
} }
@@ -429,6 +441,7 @@ export const SurveyMenuBar = ({
try { try {
const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode, t, responseCount); const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode, t, responseCount);
if (!isSurveyValidResult) { if (!isSurveyValidResult) {
isSurveyPublishingRef.current = false;
setIsSurveyPublishing(false); setIsSurveyPublishing(false);
return; return;
} }
@@ -445,10 +458,12 @@ export const SurveyMenuBar = ({
if (!publishResult?.data) { if (!publishResult?.data) {
const errorMessage = getFormattedErrorMessage(publishResult); const errorMessage = getFormattedErrorMessage(publishResult);
toast.error(errorMessage); toast.error(errorMessage);
isSurveyPublishingRef.current = false;
setIsSurveyPublishing(false); setIsSurveyPublishing(false);
return; return;
} }
isSurveyPublishingRef.current = false;
setIsSurveyPublishing(false); setIsSurveyPublishing(false);
// Set flag to prevent beforeunload warning during navigation // Set flag to prevent beforeunload warning during navigation
isSuccessfullySavedRef.current = true; isSuccessfullySavedRef.current = true;
@@ -456,6 +471,7 @@ export const SurveyMenuBar = ({
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.error(t("environments.surveys.edit.error_publishing_survey")); toast.error(t("environments.surveys.edit.error_publishing_survey"));
isSurveyPublishingRef.current = false;
setIsSurveyPublishing(false); setIsSurveyPublishing(false);
} }
}; };
@@ -202,7 +202,7 @@ function getLanguageCode(langParam: string | undefined, survey: TSurvey): string
const selectedLanguage = survey.languages.find((surveyLanguage) => { const selectedLanguage = survey.languages.find((surveyLanguage) => {
return ( return (
surveyLanguage.language.code === langParam.toLowerCase() || surveyLanguage.language.code.toLowerCase() === langParam.toLowerCase() ||
surveyLanguage.language.alias?.toLowerCase() === langParam.toLowerCase() surveyLanguage.language.alias?.toLowerCase() === langParam.toLowerCase()
); );
}); });
@@ -48,7 +48,7 @@ export function LanguageIndicator({
<button <button
aria-expanded={showLanguageDropdown} aria-expanded={showLanguageDropdown}
aria-haspopup="true" aria-haspopup="true"
className="relative z-20 flex max-w-[120px] items-center justify-center rounded-md bg-slate-900 p-1 px-2 text-xs text-white hover:bg-slate-700" className="relative z-20 flex max-w-20 items-center justify-center rounded-md bg-slate-900 p-1 px-2 text-xs text-white hover:bg-slate-700"
onClick={toggleDropdown} onClick={toggleDropdown}
tabIndex={-1} tabIndex={-1}
type="button"> type="button">
@@ -59,7 +59,7 @@ export function LanguageSwitch({
handleI18nLanguage(calculatedLanguageCode); handleI18nLanguage(calculatedLanguageCode);
if (setDir) { if (setDir) {
const calculateDir = isRTLLanguage(survey, calculatedLanguageCode) ? "rtl" : "auto"; const calculateDir = isRTLLanguage(survey, calculatedLanguageCode) ? "rtl" : "ltr";
setDir?.(calculateDir); setDir?.(calculateDir);
} }
@@ -9,12 +9,13 @@ export function RenderSurvey(props: SurveyContainerProps) {
const onFinishedTimeoutRef = useRef<NodeJS.Timeout | null>(null); const onFinishedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null); const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isRTL = isRTLLanguage(props.survey, props.languageCode); const isRTL = isRTLLanguage(props.survey, props.languageCode);
const [dir, setDir] = useState<"ltr" | "rtl" | "auto">(isRTL ? "rtl" : "auto"); const [dir, setDir] = useState<"ltr" | "rtl" | "auto">(isRTL ? "rtl" : "ltr");
useEffect(() => { useEffect(() => {
const isRTL = isRTLLanguage(props.survey, props.languageCode); const isRTL = isRTLLanguage(props.survey, props.languageCode);
setDir(isRTL ? "rtl" : "auto"); setDir(isRTL ? "rtl" : "ltr");
}, [props.languageCode, props.survey]); // eslint-disable-next-line react-hooks/exhaustive-deps -- Only recalculate direction when languageCode changes, not on survey auto-save
}, [props.languageCode]);
const close = () => { const close = () => {
if (onFinishedTimeoutRef.current) { if (onFinishedTimeoutRef.current) {
@@ -1,20 +1,28 @@
import { ComponentChildren } from "preact"; import { ComponentChildren } from "preact";
import { useEffect } from "preact/hooks"; import { useEffect, useRef } from "preact/hooks";
import { I18nextProvider } from "react-i18next"; import { I18nextProvider } from "react-i18next";
import i18n from "../../lib/i18n.config"; import i18n from "../../lib/i18n.config";
export const I18nProvider = ({ language, children }: { language: string; children?: ComponentChildren }) => { export const I18nProvider = ({ language, children }: { language: string; children?: ComponentChildren }) => {
const isFirstRender = useRef(true);
const prevLanguage = useRef(language);
// Set language synchronously on initial render so children get the correct translations immediately. // Set language synchronously on initial render so children get the correct translations immediately.
// This is safe because all translations are pre-loaded (bundled) in i18n.config.ts. // This is safe because all translations are pre-loaded (bundled) in i18n.config.ts.
if (i18n.language !== language) { // On subsequent renders, skip this to avoid overriding language changes made by the user via LanguageSwitch.
i18n.changeLanguage(language); if (isFirstRender.current) {
}
// Handle language prop changes after initial render
useEffect(() => {
if (i18n.language !== language) { if (i18n.language !== language) {
i18n.changeLanguage(language); i18n.changeLanguage(language);
} }
isFirstRender.current = false;
}
// Only update language when the prop itself changes, not when i18n was changed internally by user action
useEffect(() => {
if (prevLanguage.current !== language) {
i18n.changeLanguage(language);
prevLanguage.current = language;
}
}, [language]); }, [language]);
// work around for react-i18next not supporting preact // work around for react-i18next not supporting preact
+10 -3
View File
@@ -41,9 +41,16 @@ export const getLocalizedValue = (
* This ensures translations are always available, even when called from API routes * This ensures translations are always available, even when called from API routes
*/ */
export const getTranslations = (languageCode: string): TFunction => { export const getTranslations = (languageCode: string): TFunction => {
// "default" is a Formbricks-internal language identifier, not a valid i18next locale.
// When "default" is passed, use the current i18n language (which was already resolved
// to a real locale by the I18nProvider or LanguageSwitch). Calling
// i18n.changeLanguage("default") would cause i18next to fall back to "en", resetting
// the user's selected language (see issue #7515).
const resolvedCode = languageCode === "default" ? i18n.language : languageCode;
// Ensure the language is set (i18n.changeLanguage is synchronous when resources are already loaded) // Ensure the language is set (i18n.changeLanguage is synchronous when resources are already loaded)
if (i18n.language !== languageCode) { if (i18n.language !== resolvedCode) {
i18n.changeLanguage(languageCode); i18n.changeLanguage(resolvedCode);
} }
return i18n.getFixedT(languageCode); return i18n.getFixedT(resolvedCode);
}; };