mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-17 02:49:40 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bef8dae328 | |||
| e7e751c1c5 | |||
| d1c9b8a5a3 |
@@ -174,6 +174,13 @@ export const MultipleChoiceElementForm = ({
|
||||
|
||||
updateElement(elementIdx, {
|
||||
choices: newChoices,
|
||||
...(choiceId === "other" &&
|
||||
!element.otherOptionPlaceholder && {
|
||||
otherOptionPlaceholder: createI18nString(
|
||||
t("environments.surveys.edit.please_specify"),
|
||||
surveyLanguageCodes
|
||||
),
|
||||
}),
|
||||
...(element.shuffleOption === shuffleOptionsTypes.all.id && {
|
||||
shuffleOption: shuffleOptionsTypes.exceptLast.id,
|
||||
}),
|
||||
|
||||
+2
-2
@@ -107,7 +107,7 @@ export const ManageTranslationsModal = ({
|
||||
for (const s of strings) {
|
||||
const val = draftTranslations[s.path] ?? "";
|
||||
if (val) {
|
||||
setTranslationAtPathMutable(clone, s.path, languageCode, val);
|
||||
setTranslationAtPathMutable(clone, s.path, languageCode, val, s.value.default);
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
@@ -121,7 +121,7 @@ export const ManageTranslationsModal = ({
|
||||
const updatedSurvey = structuredClone(localSurvey);
|
||||
for (const s of strings) {
|
||||
const val = draftTranslations[s.path] ?? "";
|
||||
setTranslationAtPathMutable(updatedSurvey, s.path, languageCode, val);
|
||||
setTranslationAtPathMutable(updatedSurvey, s.path, languageCode, val, s.value.default);
|
||||
}
|
||||
setLocalSurvey(updatedSurvey);
|
||||
setOpen(false);
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { type TFunction } from "i18next";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { computeTranslationProgress, extractTranslatableStrings, setTranslationAtPathMutable } from "./utils";
|
||||
|
||||
const t = ((key: string, options?: Record<string, unknown>) => {
|
||||
const translations: Record<string, string> = {
|
||||
"common.choice_n": `Choice ${options?.n}`,
|
||||
"common.headline": "Headline",
|
||||
"common.other_placeholder": "Other Placeholder",
|
||||
"environments.surveys.edit.please_specify": "Please specify",
|
||||
};
|
||||
|
||||
return translations[key] ?? key;
|
||||
}) as unknown as TFunction;
|
||||
|
||||
const createSurvey = (survey: Record<string, unknown>): TSurvey =>
|
||||
({
|
||||
welcomeCard: { enabled: false },
|
||||
blocks: [],
|
||||
endings: [],
|
||||
metadata: {},
|
||||
...survey,
|
||||
}) as unknown as TSurvey;
|
||||
|
||||
describe("multi-language survey utils", () => {
|
||||
test("extracts missing other option placeholders for single and multi select elements", () => {
|
||||
const survey = createSurvey({
|
||||
blocks: [
|
||||
{
|
||||
id: "block-1",
|
||||
elements: [
|
||||
{
|
||||
id: "single",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Pick one" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice-1", label: { default: "One" } },
|
||||
{ id: "choice-2", label: { default: "Two" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "multi",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Pick many" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice-1", label: { default: "One" } },
|
||||
{ id: "choice-2", label: { default: "Two" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const strings = extractTranslatableStrings(survey, t);
|
||||
|
||||
expect(strings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "blocks.0.elements.0.otherOptionPlaceholder",
|
||||
fieldLabel: "Other Placeholder",
|
||||
value: { default: "Please specify" },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: "blocks.0.elements.1.otherOptionPlaceholder",
|
||||
fieldLabel: "Other Placeholder",
|
||||
value: { default: "Please specify" },
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps existing other option placeholder translations when default text is empty", () => {
|
||||
const survey = createSurvey({
|
||||
blocks: [
|
||||
{
|
||||
id: "block-1",
|
||||
elements: [
|
||||
{
|
||||
id: "single",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Pick one" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice-1", label: { default: "One" } },
|
||||
{ id: "choice-2", label: { default: "Two" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
otherOptionPlaceholder: { default: "", de: "Bitte angeben" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const placeholder = extractTranslatableStrings(survey, t).find(
|
||||
(string) => string.path === "blocks.0.elements.0.otherOptionPlaceholder"
|
||||
);
|
||||
|
||||
expect(placeholder?.value).toEqual({ default: "Please specify", de: "Bitte angeben" });
|
||||
expect(computeTranslationProgress([placeholder!], "de")).toEqual({
|
||||
translated: 1,
|
||||
total: 1,
|
||||
percentage: 100,
|
||||
});
|
||||
});
|
||||
|
||||
test("does not extract stale other option placeholders without an other choice", () => {
|
||||
const survey = createSurvey({
|
||||
blocks: [
|
||||
{
|
||||
id: "block-1",
|
||||
elements: [
|
||||
{
|
||||
id: "single",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Pick one" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice-1", label: { default: "One" } },
|
||||
{ id: "choice-2", label: { default: "Two" } },
|
||||
],
|
||||
otherOptionPlaceholder: { default: "Please specify" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
extractTranslatableStrings(survey, t).some(
|
||||
(string) => string.path === "blocks.0.elements.0.otherOptionPlaceholder"
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("creates a missing translatable field when saving a translation with a default value", () => {
|
||||
const survey = createSurvey({
|
||||
blocks: [
|
||||
{
|
||||
id: "block-1",
|
||||
elements: [
|
||||
{
|
||||
id: "single",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Pick one" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice-1", label: { default: "One" } },
|
||||
{ id: "choice-2", label: { default: "Two" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setTranslationAtPathMutable(
|
||||
survey,
|
||||
"blocks.0.elements.0.otherOptionPlaceholder",
|
||||
"de",
|
||||
"Bitte angeben",
|
||||
"Please specify"
|
||||
);
|
||||
|
||||
expect(survey.blocks[0].elements[0]).toMatchObject({
|
||||
otherOptionPlaceholder: { default: "Please specify", de: "Bitte angeben" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import { type TFunction } from "i18next";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import type { TSurveyMultipleChoiceElement, TSurveyRankingElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { isI18nObject } from "@/lib/i18n/utils";
|
||||
import type { TranslatableString, TranslationProgress } from "./types";
|
||||
|
||||
const RICH_TEXT_FIELDS = new Set(["headline", "subheader", "html"]);
|
||||
const OTHER_OPTION_PLACEHOLDER_FIELD = "otherOptionPlaceholder";
|
||||
|
||||
const pushIfI18n = (
|
||||
result: TranslatableString[],
|
||||
@@ -34,6 +36,34 @@ const pushIfI18n = (
|
||||
}
|
||||
};
|
||||
|
||||
const pushOtherOptionPlaceholder = (
|
||||
result: TranslatableString[],
|
||||
element: TSurveyMultipleChoiceElement | TSurveyRankingElement,
|
||||
path: string,
|
||||
displayId: string,
|
||||
fieldLabel: string,
|
||||
elementId: string,
|
||||
defaultPlaceholder: string
|
||||
) => {
|
||||
const hasOtherChoice = element.choices?.some((choice) => choice.id === "other");
|
||||
if (!hasOtherChoice) return;
|
||||
|
||||
const existingPlaceholder = element.otherOptionPlaceholder;
|
||||
const defaultText = existingPlaceholder?.default?.trim() ? existingPlaceholder.default : defaultPlaceholder;
|
||||
|
||||
result.push({
|
||||
path: `${path}.${OTHER_OPTION_PLACEHOLDER_FIELD}`,
|
||||
displayId,
|
||||
fieldLabel,
|
||||
value: {
|
||||
...(isI18nObject(existingPlaceholder) ? existingPlaceholder : {}),
|
||||
default: defaultText,
|
||||
},
|
||||
isRichText: false,
|
||||
elementId,
|
||||
});
|
||||
};
|
||||
|
||||
export const extractTranslatableStrings = (survey: TSurvey, t: TFunction): TranslatableString[] => {
|
||||
const result: TranslatableString[] = [];
|
||||
|
||||
@@ -108,14 +138,14 @@ export const extractTranslatableStrings = (survey: TSurvey, t: TFunction): Trans
|
||||
});
|
||||
}
|
||||
});
|
||||
pushIfI18n(
|
||||
pushOtherOptionPlaceholder(
|
||||
result,
|
||||
element,
|
||||
"otherOptionPlaceholder",
|
||||
base,
|
||||
did,
|
||||
t("common.other_placeholder"),
|
||||
eid
|
||||
eid,
|
||||
t("environments.surveys.edit.please_specify")
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -228,14 +258,14 @@ export const extractTranslatableStrings = (survey: TSurvey, t: TFunction): Trans
|
||||
});
|
||||
}
|
||||
});
|
||||
pushIfI18n(
|
||||
pushOtherOptionPlaceholder(
|
||||
result,
|
||||
element,
|
||||
"otherOptionPlaceholder",
|
||||
base,
|
||||
did,
|
||||
t("common.other_placeholder"),
|
||||
eid
|
||||
eid,
|
||||
t("environments.surveys.edit.please_specify")
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -327,7 +357,8 @@ export const setTranslationAtPathMutable = (
|
||||
survey: TSurvey,
|
||||
path: string,
|
||||
languageCode: string,
|
||||
value: string
|
||||
value: string,
|
||||
defaultValue?: string
|
||||
): void => {
|
||||
const parts = path.split(".");
|
||||
if (parts.length === 0) return;
|
||||
@@ -347,5 +378,10 @@ export const setTranslationAtPathMutable = (
|
||||
const target = current[lastPart];
|
||||
if (isTraversable(target) && !Array.isArray(target) && "default" in target) {
|
||||
(target as Record<string, string>)[languageCode] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (target === undefined && defaultValue !== undefined && value.trim() !== "") {
|
||||
current[lastPart] = { default: defaultValue, [languageCode]: value };
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user