mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-08 19:29:40 -05:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6b777db6f | |||
| 34c587342c | |||
| 5a0b421153 | |||
| af02ce9ea6 | |||
| fc1c91896a | |||
| f5c7dbdc71 | |||
| b88ea5cc66 | |||
| f31085a9e7 | |||
| 2ab0441404 | |||
| 299ae81b21 | |||
| f73f13f16c | |||
| e9bcbf6e4c | |||
| 32eda35a71 | |||
| 84999cddfd | |||
| f0a0cf531a | |||
| f3e02fa466 | |||
| f0a93ae092 | |||
| 1c922dfe2c | |||
| 33010fb6f5 | |||
| d5fdacadd7 | |||
| d939263472 | |||
| e4aa66b067 | |||
| ffcc101ed9 | |||
| 2740cd16b9 | |||
| 7eb94f0bd5 | |||
| 6dd2e707fe | |||
| 58d5de7d45 | |||
| 7c3fa8b5ea | |||
| 2601169877 | |||
| aecf85815a | |||
| c6ebaea989 | |||
| 68c1422733 | |||
| 6942502baf | |||
| f6fab9a996 | |||
| fe33527da8 | |||
| a4bd217761 | |||
| fee770358c | |||
| 44f8f80cac | |||
| 858a7f7aa9 | |||
| ac40b90e81 | |||
| aa21b4e442 | |||
| fa72296de5 |
@@ -1,2 +0,0 @@
|
||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json
|
||||
prettier --write ./branch.json
|
||||
+12
-17
@@ -10,25 +10,20 @@
|
||||
"build-storybook": "storybook build",
|
||||
"clean": "rimraf .turbo node_modules dist storybook-static"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/survey-ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^5.0.0",
|
||||
"@storybook/addon-a11y": "10.1.11",
|
||||
"@storybook/addon-links": "10.1.11",
|
||||
"@storybook/addon-onboarding": "10.1.11",
|
||||
"@storybook/react-vite": "10.1.11",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@tailwindcss/vite": "4.1.18",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"esbuild": "0.25.12",
|
||||
"@chromatic-com/storybook": "^5.0.1",
|
||||
"@storybook/addon-a11y": "10.2.14",
|
||||
"@storybook/addon-links": "10.2.14",
|
||||
"@storybook/addon-onboarding": "10.2.14",
|
||||
"@storybook/react-vite": "10.2.14",
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@tailwindcss/vite": "4.2.1",
|
||||
"@typescript-eslint/parser": "8.56.1",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"eslint-plugin-react-refresh": "0.4.26",
|
||||
"eslint-plugin-storybook": "10.1.11",
|
||||
"prop-types": "15.8.1",
|
||||
"storybook": "10.1.11",
|
||||
"eslint-plugin-storybook": "10.2.14",
|
||||
"storybook": "10.2.14",
|
||||
"vite": "7.3.1",
|
||||
"@storybook/addon-docs": "10.1.11"
|
||||
"@storybook/addon-docs": "10.2.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
const baseConfig = require("../../.prettierrc.js");
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
tailwindConfig: "./tailwind.config.js",
|
||||
};
|
||||
@@ -101,6 +101,9 @@ RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
|
||||
# Create packages/database directory structure with proper ownership for runtime migrations
|
||||
RUN mkdir -p ./packages/database/migrations && chown -R nextjs:nextjs ./packages/database
|
||||
|
||||
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
|
||||
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
|
||||
|
||||
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
|
||||
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ export const ConnectWithFormbricks = ({
|
||||
) : (
|
||||
<div className="flex animate-pulse flex-col items-center space-y-4">
|
||||
<span className="relative flex h-10 w-10">
|
||||
<span className="animate-ping-slow absolute inline-flex h-full w-full rounded-full bg-slate-400 opacity-75"></span>
|
||||
<span className="absolute inline-flex h-full w-full animate-ping-slow rounded-full bg-slate-400 opacity-75"></span>
|
||||
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
|
||||
</span>
|
||||
<p className="pt-4 text-sm font-medium text-slate-600">
|
||||
|
||||
@@ -46,7 +46,7 @@ const Page = async (props: ConnectPageProps) => {
|
||||
channel={channel}
|
||||
/>
|
||||
<Button
|
||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={`/environments/${environment.id}`}>
|
||||
|
||||
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
|
||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||
{projects.length >= 2 && (
|
||||
<Button
|
||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={`/environments/${environment.id}/surveys`}>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"w-sidebar-collapsed z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
|
||||
"z-40 flex w-sidebar-collapsed flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
|
||||
)}>
|
||||
<Image src={FBLogo} width={160} height={30} alt={t("environments.formbricks_logo")} />
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={"/"}>
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={"/"}>
|
||||
|
||||
+11
-13
@@ -228,7 +228,7 @@ export const ProjectSettings = ({
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
||||
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow">
|
||||
<div className="relative flex w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 p-6 shadow">
|
||||
{logoUrl && (
|
||||
<Image
|
||||
src={logoUrl}
|
||||
@@ -239,18 +239,16 @@ export const ProjectSettings = ({
|
||||
/>
|
||||
)}
|
||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||
<div className="z-0 h-3/4 w-3/4">
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(projectName || "my Product", t)}
|
||||
styling={previewStyling}
|
||||
isBrandingEnabled={false}
|
||||
languageCode="default"
|
||||
onFileUpload={async (file) => file.name}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</div>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(projectName || t("common.my_product"), t)}
|
||||
styling={previewStyling}
|
||||
isBrandingEnabled={false}
|
||||
languageCode="default"
|
||||
onFileUpload={async (file) => file.name}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</div>
|
||||
<CreateTeamModal
|
||||
open={createTeamModalOpen}
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
||||
/>
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={"/"}>
|
||||
|
||||
@@ -188,7 +188,7 @@ export const MainNavigation = ({
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
className={cn(
|
||||
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
|
||||
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:outline-none focus:ring-0 focus:ring-transparent"
|
||||
)}>
|
||||
{isCollapsed ? (
|
||||
<PanelLeftOpenIcon strokeWidth={1.5} />
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
||||
<currentStatus.icon />
|
||||
</div>
|
||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
||||
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
|
||||
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
|
||||
{status === "notImplemented" && (
|
||||
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
||||
<RotateCcwIcon />
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ export const OrganizationBreadcrumb = ({
|
||||
getOrganizationsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
|
||||
if (result?.data) {
|
||||
// Sort organizations by name
|
||||
const sorted = result.data.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
|
||||
setOrganizations(sorted);
|
||||
} else {
|
||||
// Handle server errors or validation errors
|
||||
|
||||
@@ -82,7 +82,7 @@ export const ProjectBreadcrumb = ({
|
||||
getProjectsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
|
||||
if (result?.data) {
|
||||
// Sort projects by name
|
||||
const sorted = result.data.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
|
||||
setProjects(sorted);
|
||||
} else {
|
||||
// Handle server errors or validation errors
|
||||
|
||||
+49
-36
@@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { TUser, TUserUpdateInput, ZUser, ZUserEmail } from "@formbricks/types/user";
|
||||
import { PasswordConfirmationModal } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/password-confirmation-modal";
|
||||
import { appLanguages } from "@/lib/i18n/utils";
|
||||
import { appLanguages, sortedAppLanguages } from "@/lib/i18n/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -198,41 +198,54 @@ export const EditProfileDetailsForm = ({
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="locale"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4">
|
||||
<FormLabel>{t("common.language")}</FormLabel>
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-10 w-full border border-slate-300 px-3 text-left">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{appLanguages.find((l) => l.code === field.value)?.label["en-US"] ?? "NA"}
|
||||
<ChevronDownIcon className="h-4 w-4 text-slate-500" />
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[var(--radix-dropdown-menu-trigger-width)] bg-white text-slate-700"
|
||||
align="start">
|
||||
<DropdownMenuRadioGroup value={field.value} onValueChange={field.onChange}>
|
||||
{appLanguages.map((lang) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={lang.code}
|
||||
value={lang.code}
|
||||
className="min-h-8 cursor-pointer">
|
||||
{lang.label["en-US"]}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
<FormError />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const selectedLanguage = appLanguages.find((l) => l.code === field.value);
|
||||
|
||||
return (
|
||||
<FormItem className="mt-4">
|
||||
<FormLabel>{t("common.language")}</FormLabel>
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="h-10 w-full border border-slate-300 px-3 text-left">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{selectedLanguage ? (
|
||||
<>
|
||||
{selectedLanguage.label["en-US"]}
|
||||
{selectedLanguage.label.native !== selectedLanguage.label["en-US"] &&
|
||||
` (${selectedLanguage.label.native})`}
|
||||
</>
|
||||
) : (
|
||||
t("common.select")
|
||||
)}
|
||||
<ChevronDownIcon className="h-4 w-4 text-slate-500" />
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[var(--radix-dropdown-menu-trigger-width)] bg-white text-slate-700"
|
||||
align="start">
|
||||
<DropdownMenuRadioGroup value={field.value} onValueChange={field.onChange}>
|
||||
{sortedAppLanguages.map((lang) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={lang.code}
|
||||
value={lang.code}
|
||||
className="min-h-8 cursor-pointer">
|
||||
{lang.label["en-US"]}
|
||||
{lang.label.native !== lang.label["en-US"] && ` (${lang.label.native})`}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
<FormError />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{isPasswordResetEnabled && (
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ export const PasswordConfirmationModal = ({
|
||||
aria-label="password"
|
||||
aria-required="true"
|
||||
required
|
||||
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
|
||||
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
|
||||
value={field.value}
|
||||
onChange={(password) => field.onChange(password)}
|
||||
/>
|
||||
|
||||
+5
-1
@@ -9,6 +9,7 @@ import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import packageJson from "@/package.json";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
||||
@@ -81,7 +82,10 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
<IdBadge id={organization.id} label={t("common.organization_id")} variant="column" />
|
||||
<div className="space-y-2">
|
||||
<IdBadge id={organization.id} label={t("common.organization_id")} variant="column" />
|
||||
<IdBadge id={packageJson.version} label={t("common.formbricks_version")} variant="column" />
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-1
@@ -3,6 +3,7 @@ import { getServerSession } from "next-auth";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
|
||||
type Props = {
|
||||
@@ -14,10 +15,11 @@ export const generateMetadata = async (props: Props): Promise<Metadata> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const survey = await getSurvey(params.surveyId);
|
||||
const responseCount = await getResponseCountBySurveyId(params.surveyId);
|
||||
const t = await getTranslate();
|
||||
|
||||
if (session) {
|
||||
return {
|
||||
title: `${responseCount} Responses | ${survey?.name} Results`,
|
||||
title: `${t("common.count_responses", { count: responseCount })} | ${t("environments.surveys.summary.survey_results", { surveyName: survey?.name })}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
+2
-4
@@ -30,8 +30,7 @@ export const CalSummary = ({ elementSummary, survey }: CalSummaryProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{elementSummary.booked.count}{" "}
|
||||
{elementSummary.booked.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{t("common.count_responses", { count: elementSummary.booked.count })}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.booked.percentage / 100} />
|
||||
@@ -47,8 +46,7 @@ export const CalSummary = ({ elementSummary, survey }: CalSummaryProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{elementSummary.skipped.count}{" "}
|
||||
{elementSummary.skipped.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{t("common.count_responses", { count: elementSummary.skipped.count })}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.skipped.percentage / 100} />
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export const ConsentSummary = ({ elementSummary, survey, setFilter }: ConsentSum
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{summaryItem.count} {summaryItem.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{t("common.count_responses", { count: summaryItem.count })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="group-hover:opacity-80">
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export const ElementSummaryHeader = ({
|
||||
{showResponses && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${elementSummary.responseCount} ${t("common.responses")}`}
|
||||
{t("common.count_responses", { count: elementSummary.responseCount })}
|
||||
</div>
|
||||
)}
|
||||
{additionalInfo}
|
||||
|
||||
+1
-2
@@ -41,8 +41,7 @@ export const HiddenFieldsSummary = ({ environment, elementSummary, locale }: Hid
|
||||
</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{elementSummary.responseCount}{" "}
|
||||
{elementSummary.responseCount === 1 ? t("common.response") : t("common.responses")}
|
||||
{t("common.count_responses", { count: elementSummary.responseCount })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ export const MatrixElementSummary = ({ elementSummary, survey, setFilter }: Matr
|
||||
if (label) {
|
||||
return label;
|
||||
} else if (percentage !== undefined && totalResponsesForRow !== undefined) {
|
||||
return `${Math.round((percentage / 100) * totalResponsesForRow)} ${t("common.responses")}`;
|
||||
return t("common.count_responses", { count: Math.round((percentage / 100) * totalResponsesForRow) });
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@@ -77,7 +77,7 @@ export const MatrixElementSummary = ({ elementSummary, survey, setFilter }: Matr
|
||||
)}>
|
||||
<button
|
||||
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
|
||||
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline"
|
||||
className="m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline hover:outline-brand-dark"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
elementSummary.element.id,
|
||||
|
||||
+2
-2
@@ -75,7 +75,7 @@ export const MultipleChoiceSummary = ({
|
||||
elementSummary.type === "multipleChoiceMulti" ? (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${elementSummary.selectionCount} ${t("common.selections")}`}
|
||||
{t("common.count_selections", { count: elementSummary.selectionCount })}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export const MultipleChoiceSummary = ({
|
||||
</div>
|
||||
<div className="flex w-full space-x-2">
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
{t("common.count_selections", { count: result.count })}
|
||||
</p>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
|
||||
+2
-3
@@ -123,8 +123,7 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{elementSummary[group]?.count}{" "}
|
||||
{elementSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{t("common.count_responses", { count: elementSummary[group]?.count })}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar
|
||||
@@ -158,7 +157,7 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
|
||||
}>
|
||||
<div className="flex h-32 w-full flex-col items-center justify-end">
|
||||
<div
|
||||
className="bg-brand-dark w-full rounded-t-lg border border-slate-200 transition-all group-hover:brightness-110"
|
||||
className="w-full rounded-t-lg border border-slate-200 bg-brand-dark transition-all group-hover:brightness-110"
|
||||
style={{
|
||||
height: `${Math.max(choice.percentage, 2)}%`,
|
||||
opacity,
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ export const PictureChoiceSummary = ({ elementSummary, survey, setFilter }: Pict
|
||||
elementSummary.element.allowMulti ? (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${elementSummary.selectionCount} ${t("common.selections")}`}
|
||||
{t("common.count_selections", { count: elementSummary.selectionCount })}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export const PictureChoiceSummary = ({ elementSummary, survey, setFilter }: Pict
|
||||
</div>
|
||||
<div className="flex w-full space-x-2">
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
|
||||
{t("common.count_selections", { count: result.count })}
|
||||
</p>
|
||||
<p className="self-end rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
|
||||
+3
-4
@@ -116,7 +116,7 @@ export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSumma
|
||||
)
|
||||
}>
|
||||
<div
|
||||
className={`bg-brand-dark h-full ${isFirst ? "rounded-tl-lg" : ""} ${isLast ? "rounded-tr-lg" : ""}`}
|
||||
className={`h-full bg-brand-dark ${isFirst ? "rounded-tl-lg" : ""} ${isLast ? "rounded-tr-lg" : ""}`}
|
||||
style={{ opacity }}
|
||||
/>
|
||||
</ClickableBarSegment>
|
||||
@@ -198,7 +198,7 @@ export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSumma
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{result.count} {result.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{t("common.count_responses", { count: result.count })}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
|
||||
@@ -215,8 +215,7 @@ export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSumma
|
||||
<div className="text flex justify-between px-2">
|
||||
<p className="font-semibold text-slate-700">{t("common.dismissed")}</p>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{elementSummary.dismissed.count}{" "}
|
||||
{elementSummary.dismissed.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{t("common.count_responses", { count: elementSummary.dismissed.count })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -352,7 +352,7 @@ export const AnonymousLinksTab = ({
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.share.anonymous_links.custom_start_point"),
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-question",
|
||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-block",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -105,7 +105,7 @@ export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTa
|
||||
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
|
||||
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<pre className="font-mono text-xs whitespace-pre-wrap text-slate-600">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs text-slate-600">
|
||||
{projectCustomScripts}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -135,7 +135,7 @@ export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTa
|
||||
rows={8}
|
||||
placeholder={t("environments.surveys.share.custom_html.placeholder")}
|
||||
className={cn(
|
||||
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
"flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:border-brand-dark focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
)}
|
||||
{...field}
|
||||
disabled={isReadOnly}
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ export const SuccessView: React.FC<SuccessViewProps> = ({
|
||||
className="relative flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
|
||||
<UserIcon className="h-8 w-8 stroke-1 text-slate-900" />
|
||||
{t("environments.surveys.summary.use_personal_links")}
|
||||
<Badge size="normal" type="success" className="absolute top-3 right-3" text={t("common.new")} />
|
||||
<Badge size="normal" type="success" className="absolute right-3 top-3" text={t("common.new")} />
|
||||
</button>
|
||||
<Link
|
||||
href={`/environments/${environmentId}/settings/notifications`}
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementCo
|
||||
value={inputValue}
|
||||
onValueChange={setInputValue}
|
||||
placeholder={open ? `${t("common.search")}...` : t("common.select_filter")}
|
||||
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none ring-offset-transparent outline-none focus:border-none focus:shadow-none focus:ring-offset-0 focus:outline-none"
|
||||
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none outline-none ring-offset-transparent focus:border-none focus:shadow-none focus:outline-none focus:ring-offset-0"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ const Loading = () => {
|
||||
<div className="mt-6 p-6">
|
||||
<GoBackButton />
|
||||
<div className="mb-6 text-right">
|
||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
|
||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
|
||||
{t("environments.integrations.google_sheets.link_new_sheet")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@ const Loading = () => {
|
||||
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
|
||||
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ const Loading = () => {
|
||||
<div className="mt-6 p-6">
|
||||
<GoBackButton />
|
||||
<div className="mb-6 text-right">
|
||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
|
||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
|
||||
{t("environments.integrations.notion.link_database")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -48,7 +48,7 @@ const Loading = () => {
|
||||
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
|
||||
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
||||
import { convertDatesInObject } from "@/lib/time";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
import { queueAuditEvent } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { TAuditStatus, UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
||||
import { sendResponseFinishedEmail } from "@/modules/email";
|
||||
@@ -135,13 +136,17 @@ export const POST = async (request: Request) => {
|
||||
);
|
||||
}
|
||||
|
||||
return fetchWithTimeout(webhook.url, {
|
||||
method: "POST",
|
||||
headers: requestHeaders,
|
||||
body,
|
||||
}).catch((error) => {
|
||||
logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
|
||||
});
|
||||
return validateWebhookUrl(webhook.url)
|
||||
.then(() =>
|
||||
fetchWithTimeout(webhook.url, {
|
||||
method: "POST",
|
||||
headers: requestHeaders,
|
||||
body,
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
|
||||
});
|
||||
});
|
||||
|
||||
if (event === "responseFinished") {
|
||||
|
||||
@@ -6,140 +6,138 @@ export const GET = async (req: NextRequest) => {
|
||||
let brandColor = req.nextUrl.searchParams.get("brandColor");
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
backgroundColor: brandColor ? brandColor + "BF" : "#0000BFBF", // /75 opacity is approximately BF in hex
|
||||
borderRadius: "0.75rem",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
backgroundColor: brandColor ? brandColor + "BF" : "#0000BFBF", // /75 opacity is approximately BF in hex
|
||||
width: "80%",
|
||||
height: "60%",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "0.75rem",
|
||||
marginTop: "3.25rem",
|
||||
position: "absolute",
|
||||
left: "3rem",
|
||||
top: "0.75rem",
|
||||
opacity: 0.2,
|
||||
transform: "rotate(356deg)",
|
||||
}}></div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "84%",
|
||||
height: "60%",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "0.75rem",
|
||||
marginTop: "3rem",
|
||||
position: "absolute",
|
||||
top: "1.25rem",
|
||||
left: "3.25rem",
|
||||
borderWidth: "2px",
|
||||
opacity: 0.6,
|
||||
transform: "rotate(357deg)",
|
||||
}}></div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "85%",
|
||||
height: "67%",
|
||||
alignItems: "center",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "0.75rem",
|
||||
marginTop: "2rem",
|
||||
position: "absolute",
|
||||
top: "2.3rem",
|
||||
left: "3.5rem",
|
||||
transform: "rotate(360deg)",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "80%",
|
||||
height: "60%",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "0.75rem",
|
||||
marginTop: "3.25rem",
|
||||
position: "absolute",
|
||||
left: "3rem",
|
||||
top: "0.75rem",
|
||||
opacity: 0.2,
|
||||
transform: "rotate(356deg)",
|
||||
}}></div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "84%",
|
||||
height: "60%",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "0.75rem",
|
||||
marginTop: "3rem",
|
||||
position: "absolute",
|
||||
top: "1.25rem",
|
||||
left: "3.25rem",
|
||||
borderWidth: "2px",
|
||||
opacity: 0.6,
|
||||
transform: "rotate(357deg)",
|
||||
}}></div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "85%",
|
||||
height: "67%",
|
||||
alignItems: "center",
|
||||
backgroundColor: "white",
|
||||
borderRadius: "0.75rem",
|
||||
marginTop: "2rem",
|
||||
position: "absolute",
|
||||
top: "2.3rem",
|
||||
left: "3.5rem",
|
||||
transform: "rotate(360deg)",
|
||||
}}>
|
||||
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
paddingLeft: "2rem",
|
||||
paddingRight: "2rem",
|
||||
}}>
|
||||
<h2
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
fontSize: "2rem",
|
||||
fontWeight: "700",
|
||||
letterSpacing: "-0.025em",
|
||||
color: "#0f172a",
|
||||
textAlign: "left",
|
||||
marginTop: "3.75rem",
|
||||
}}>
|
||||
{name}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", marginRight: "2.5rem" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
borderRadius: "1rem",
|
||||
position: "absolute",
|
||||
right: "-0.5rem",
|
||||
marginTop: "0.5rem",
|
||||
}}>
|
||||
<div
|
||||
content=""
|
||||
style={{
|
||||
borderRadius: "0.75rem",
|
||||
border: "1px solid transparent",
|
||||
backgroundColor: brandColor ?? "#000",
|
||||
height: "4.5rem",
|
||||
width: "9.5rem",
|
||||
opacity: 0.5,
|
||||
}}></div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
paddingLeft: "2rem",
|
||||
paddingRight: "2rem",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "0.75rem",
|
||||
border: "1px solid transparent",
|
||||
backgroundColor: brandColor ?? "#000",
|
||||
fontSize: "1.5rem",
|
||||
color: "white",
|
||||
height: "4.5rem",
|
||||
width: "9.5rem",
|
||||
}}>
|
||||
<h2
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
fontSize: "2rem",
|
||||
fontWeight: "700",
|
||||
letterSpacing: "-0.025em",
|
||||
color: "#0f172a",
|
||||
textAlign: "left",
|
||||
marginTop: "3.75rem",
|
||||
}}>
|
||||
{name}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", marginRight: "2.5rem" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
borderRadius: "1rem",
|
||||
position: "absolute",
|
||||
right: "-0.5rem",
|
||||
marginTop: "0.5rem",
|
||||
}}>
|
||||
<div
|
||||
content=""
|
||||
style={{
|
||||
borderRadius: "0.75rem",
|
||||
border: "1px solid transparent",
|
||||
backgroundColor: brandColor ?? "#000",
|
||||
height: "4.5rem",
|
||||
width: "9.5rem",
|
||||
opacity: 0.5,
|
||||
}}></div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "0.75rem",
|
||||
border: "1px solid transparent",
|
||||
backgroundColor: brandColor ?? "#000",
|
||||
fontSize: "1.5rem",
|
||||
color: "white",
|
||||
height: "4.5rem",
|
||||
width: "9.5rem",
|
||||
}}>
|
||||
Begin!
|
||||
</div>
|
||||
Begin!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
</div>,
|
||||
{
|
||||
width: 800,
|
||||
height: 400,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@formbricks/types/integration/slack";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { TSessionAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_REDIRECT_URI, WEBAPP_URL } from "@/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||
import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service";
|
||||
|
||||
@@ -56,6 +56,7 @@ export const GET = withV1ApiWrapper({
|
||||
code,
|
||||
client_id: SLACK_CLIENT_ID,
|
||||
client_secret: SLACK_CLIENT_SECRET,
|
||||
redirect_uri: SLACK_REDIRECT_URI,
|
||||
};
|
||||
const formBody: string[] = [];
|
||||
for (const property in formData) {
|
||||
|
||||
@@ -2,10 +2,11 @@ import { Prisma, WebhookSource } from "@prisma/client";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
|
||||
import { DatabaseError, InvalidInputError, ValidationError } from "@formbricks/types/errors";
|
||||
import { createWebhook } from "@/app/api/v1/webhooks/lib/webhook";
|
||||
import { TWebhookInput } from "@/app/api/v1/webhooks/types/webhooks";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
@@ -23,6 +24,10 @@ vi.mock("@/lib/crypto", () => ({
|
||||
generateWebhookSecret: vi.fn(() => "whsec_test_secret_1234567890"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/validate-webhook-url", () => ({
|
||||
validateWebhookUrl: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
describe("createWebhook", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -75,6 +80,41 @@ describe("createWebhook", () => {
|
||||
expect(result).toEqual(createdWebhook);
|
||||
});
|
||||
|
||||
test("should call validateWebhookUrl with the provided URL", async () => {
|
||||
const webhookInput: TWebhookInput = {
|
||||
environmentId: "test-env-id",
|
||||
name: "Test Webhook",
|
||||
url: "https://example.com",
|
||||
source: "user",
|
||||
triggers: ["responseCreated"],
|
||||
surveyIds: ["survey1"],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.webhook.create).mockResolvedValueOnce({} as any);
|
||||
|
||||
await createWebhook(webhookInput);
|
||||
|
||||
expect(validateWebhookUrl).toHaveBeenCalledWith("https://example.com");
|
||||
});
|
||||
|
||||
test("should throw InvalidInputError and skip Prisma create when URL fails SSRF validation", async () => {
|
||||
const webhookInput: TWebhookInput = {
|
||||
environmentId: "test-env-id",
|
||||
name: "Test Webhook",
|
||||
url: "http://169.254.169.254/latest/meta-data/",
|
||||
source: "user",
|
||||
triggers: ["responseCreated"],
|
||||
surveyIds: ["survey1"],
|
||||
};
|
||||
|
||||
vi.mocked(validateWebhookUrl).mockRejectedValueOnce(
|
||||
new InvalidInputError("Webhook URL must not point to private or internal IP addresses")
|
||||
);
|
||||
|
||||
await expect(createWebhook(webhookInput)).rejects.toThrow(InvalidInputError);
|
||||
expect(prisma.webhook.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should throw a ValidationError if the input data does not match the ZWebhookInput schema", async () => {
|
||||
const invalidWebhookInput = {
|
||||
environmentId: "test-env-id",
|
||||
|
||||
@@ -6,9 +6,11 @@ import { TWebhookInput, ZWebhookInput } from "@/app/api/v1/webhooks/types/webhoo
|
||||
import { ITEMS_PER_PAGE } from "@/lib/constants";
|
||||
import { generateWebhookSecret } from "@/lib/crypto";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
|
||||
export const createWebhook = async (webhookInput: TWebhookInput): Promise<Webhook> => {
|
||||
validateInputs([webhookInput, ZWebhookInput]);
|
||||
await validateWebhookUrl(webhookInput.url);
|
||||
|
||||
try {
|
||||
const secret = generateWebhookSecret();
|
||||
|
||||
@@ -131,13 +131,11 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
|
||||
test("logs and audits on error response with API key authentication", async () => {
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
||||
"@/modules/ee/audit-logs/lib/handler"
|
||||
)) as unknown as { queueAuditEvent: Mock };
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } =
|
||||
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
@@ -185,13 +183,11 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
|
||||
test("does not log Sentry if not 500", async () => {
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
||||
"@/modules/ee/audit-logs/lib/handler"
|
||||
)) as unknown as { queueAuditEvent: Mock };
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } =
|
||||
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
@@ -233,13 +229,11 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
|
||||
test("logs and audits on thrown error", async () => {
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
||||
"@/modules/ee/audit-logs/lib/handler"
|
||||
)) as unknown as { queueAuditEvent: Mock };
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } =
|
||||
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
@@ -291,13 +285,11 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
|
||||
test("does not log on success response but still audits", async () => {
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
||||
"@/modules/ee/audit-logs/lib/handler"
|
||||
)) as unknown as { queueAuditEvent: Mock };
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } =
|
||||
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
@@ -347,13 +339,11 @@ describe("withV1ApiWrapper", () => {
|
||||
REDIS_URL: "redis://localhost:6379",
|
||||
}));
|
||||
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
||||
"@/modules/ee/audit-logs/lib/handler"
|
||||
)) as unknown as { queueAuditEvent: Mock };
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } =
|
||||
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { withV1ApiWrapper } = await import("./with-api-logging");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
|
||||
@@ -376,9 +366,8 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
|
||||
test("handles client-side API routes without authentication", async () => {
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||
|
||||
@@ -410,9 +399,8 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
|
||||
test("returns authentication error for non-client routes without auth", async () => {
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
@@ -435,9 +423,8 @@ describe("withV1ApiWrapper", () => {
|
||||
|
||||
test("handles rate limiting errors", async () => {
|
||||
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
|
||||
@@ -462,13 +449,11 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
|
||||
test("skips audit log creation when no action/targetType provided", async () => {
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
||||
"@/modules/ee/audit-logs/lib/handler"
|
||||
)) as unknown as { queueAuditEvent: Mock };
|
||||
const { queueAuditEvent: mockedQueueAuditEvent } =
|
||||
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
|
||||
"@/app/middleware/endpoint-validator"
|
||||
);
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
|
||||
+221
-210
File diff suppressed because it is too large
Load Diff
@@ -313,7 +313,7 @@ describe("endpoint-validator", () => {
|
||||
expect(isPublicDomainRoute("/c")).toBe(false);
|
||||
expect(isPublicDomainRoute("/contact/token")).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
test("should return true for pretty URL survey routes", () => {
|
||||
expect(isPublicDomainRoute("/p/pretty123")).toBe(true);
|
||||
expect(isPublicDomainRoute("/p/pretty-name-with-dashes")).toBe(true);
|
||||
|
||||
+40
-13
@@ -148,9 +148,12 @@ checksums:
|
||||
common/copy: 627c00d2c850b9b45f7341a6ac01b6bb
|
||||
common/copy_code: 704c13d9bc01caad29a1cf3179baa111
|
||||
common/copy_link: 57a37acfe6d7ed71d00fbbc8079fbb35
|
||||
common/count_attributes: 042fba9baffef5afe2c24f13d4f50697
|
||||
common/count_contacts: b1c413a4b06961b71b6aeee95d6775d7
|
||||
common/count_responses: 690118a456c01c5b4d437ae82b50b131
|
||||
common/count_attributes: 48805e836a9b50f9635ad00fed953058
|
||||
common/count_contacts: 9f71d503455264f1eec1ae58894cf143
|
||||
common/count_members: 31ce64ca63fdf95e02ab5543b6e2f717
|
||||
common/count_questions: a7a34376a01eda781381fe7544541293
|
||||
common/count_responses: 437e022825c7a08481d8f7e56926742d
|
||||
common/count_selections: a1ec41682b9a7d8601c3905dfba34e16
|
||||
common/create_new_organization: 51dae7b33143686ee218abf5bea764a5
|
||||
common/create_segment: 9d8291cd4d778b53b73bbc84fd91c181
|
||||
common/create_survey: 1cfbba08d34876566d84b2960054a987
|
||||
@@ -164,6 +167,7 @@ checksums:
|
||||
common/days: c95fe8aedde21a0b5653dbd0b3c58b48
|
||||
common/default: d9c6dc5c412fe94143dfd1d332ec81d4
|
||||
common/delete: 8bcf303dd10a645b5baacb02b47d72c9
|
||||
common/delete_what: 718ddfcc1dec7f3e8b67856fba838267
|
||||
common/description: e17686a22ffad04cc7bb70524ed4478b
|
||||
common/dev_env: e650911d5e19ba256358e0cda154c005
|
||||
common/development: 85211dbb918bda7a6e87649dcfc1b17a
|
||||
@@ -179,6 +183,8 @@ checksums:
|
||||
common/download: 56b7d0834952b39ee394b44bd8179178
|
||||
common/draft: e8a92958ad300aacfe46c2bf6644927e
|
||||
common/duplicate: 27756566785c2b8463e21582c4bb619b
|
||||
common/duplicate_copy: 68d2201918610ca87c2914b61dc8010f
|
||||
common/duplicate_copy_number: 083cfffd294672043dcbcc4c3dfeac6a
|
||||
common/e_commerce: b9584e7d0449a6d1b0c182d7ff14061e
|
||||
common/edit: eee7f39ff90b18852afc1671f21fbaa9
|
||||
common/email: e7f34943a0c2fb849db1839ff6ef5cb5
|
||||
@@ -198,7 +204,9 @@ checksums:
|
||||
common/failed_to_copy_to_clipboard: de836a7d628d36c832809252f188f784
|
||||
common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e
|
||||
common/failed_to_load_workspaces: 6ee3448097394517dc605074cd4e6ea4
|
||||
common/filter: 626325a05e4c8800f7ede7012b0cadaf
|
||||
common/finish: ffa7a10f71182b48fefed7135bee24fa
|
||||
common/first_name: cf040a5d6a9fd696be400380cc99f54b
|
||||
common/follow_these: 3a730b242bb17a3f95e01bf0dae86885
|
||||
common/formbricks_version: d9967c797f3e49ca0cae78bc0ebd19cb
|
||||
common/full_name: f45991923345e8322c9ff8cd6b7e2b16
|
||||
@@ -211,6 +219,7 @@ checksums:
|
||||
common/hidden_field: 3ed5c58d0ed359e558cdf7bd33606d2d
|
||||
common/hidden_fields: 3de6cfd308293a826cb8679fd1d49972
|
||||
common/hide_column: 23ce94db148f2d8e4a0923defead6cf1
|
||||
common/id: c8886d38aeea2ed5f785aba4fc96784b
|
||||
common/image: 048ba7a239de0fbd883ade8558415830
|
||||
common/images: 9305827c28694866f49db42b4c51831f
|
||||
common/import: 348b8ab981de5b7f1fca6d7302263bbd
|
||||
@@ -228,6 +237,7 @@ checksums:
|
||||
common/key: 3d1065ab98a1c2f1210507fd5c7bf515
|
||||
common/label: a5c71bf158481233f8215dbd38cc196b
|
||||
common/language: 277fd1a41cc237a437cd1d5e4a80463b
|
||||
common/last_name: 2c9a7de7738ca007ba9023c385149c26
|
||||
common/learn_more: e598091d132f890c37a6d4ed94f6d794
|
||||
common/license_expired: 7af13535e320e4197989472c01387d2c
|
||||
common/light_overlay: 0499907ea7b8405f4267b117998b5a78
|
||||
@@ -242,7 +252,6 @@ checksums:
|
||||
common/look_and_feel: 9125503712626d495cedec7a79f1418c
|
||||
common/manage: a3d40c0267b81ae53c9598eaeb05087d
|
||||
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||
common/member: 1606dc30b369856b9dba1fe9aec425d2
|
||||
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
||||
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
||||
@@ -254,6 +263,7 @@ checksums:
|
||||
common/move_down: 4f4de55743043355ad4a839aff2c48ff
|
||||
common/move_up: 69f25b205c677abdb26cbb69d97cd10b
|
||||
common/multiple_languages: 7d8ddd4b40d32fcd7bd6f7bac6485b1f
|
||||
common/my_product: ad022177062f9ef6e9acf33b13e889aa
|
||||
common/name: 9368b5a047572b6051f334af5aa76819
|
||||
common/new: 126d036fae5fb6b629728ecb97e6195b
|
||||
common/new_version_available: 399ddfc4232712e18ddab2587356b3dc
|
||||
@@ -349,8 +359,6 @@ checksums:
|
||||
common/select_teams: ae5d451929846ae6367562bc671a1af9
|
||||
common/selected: 9f09e059ba20c88ed34e2b4e8e032d56
|
||||
common/selected_questions: beffe92d5272d99a0022f004e6a6ad73
|
||||
common/selection: 25b570dc6339916a7aada2142aca0cd1
|
||||
common/selections: 82f0681bf0208e25d7efedc23c556b8f
|
||||
common/send_test_email: 2fd3ea40199b9589132ac826a5b0f3f5
|
||||
common/session_not_found: e9622df3170dbfd9636403bb0c22295b
|
||||
common/settings: 8df6777277469c1fd88cc18dde2f1cc3
|
||||
@@ -1016,7 +1024,7 @@ checksums:
|
||||
environments/settings/general/email_customization_preview_email_heading: 8b798cb8438b3dd356c02dab33b4c897
|
||||
environments/settings/general/email_customization_preview_email_text: fa6ae92403cc8f3c35c03e6c94cbde51
|
||||
environments/settings/general/error_deleting_organization_please_try_again: 7f0fe257d4a0b40bff025408a7766706
|
||||
environments/settings/general/from_your_organization: 4b7970431edb3d0f13c394dbd755a055
|
||||
environments/settings/general/from_your_organization: 9ebd6dcd79f7bfad3fea46ed2e3133d2
|
||||
environments/settings/general/invitation_sent_once_more: e6e5ea066810f9dcb65788aa4f05d6e2
|
||||
environments/settings/general/invite_deleted_successfully: 1c7dca6d0f6870d945288e38cfd2f943
|
||||
environments/settings/general/invite_expires_on: 6fd2356ad91a5f189070c43855904bb4
|
||||
@@ -1171,6 +1179,7 @@ checksums:
|
||||
environments/surveys/edit/add_fallback_placeholder: 0e77ea487ddd7bc7fc2f1574b018dc08
|
||||
environments/surveys/edit/add_hidden_field_id: a8f55b51b790cf5f4d898af7770ad1ed
|
||||
environments/surveys/edit/add_highlight_border: 66f52b21fbb9aa6561c98a090abaaf8f
|
||||
environments/surveys/edit/add_highlight_border_description: fe548fe03ea10ef5cd9e553d6812b3c2
|
||||
environments/surveys/edit/add_logic: f234c9f1393a9ed4792dfbd15838c951
|
||||
environments/surveys/edit/add_none_of_the_above: dbe1ada4512d6c3f80c54c8fac107ec6
|
||||
environments/surveys/edit/add_option: 143c54f0b201067fe5159284d6daeca2
|
||||
@@ -1187,11 +1196,13 @@ checksums:
|
||||
environments/surveys/edit/adjust_survey_closed_message: ae6f38c9daf08656362bd84459a312fa
|
||||
environments/surveys/edit/adjust_survey_closed_message_description: e906aebd9af6451a2a39c73287927299
|
||||
environments/surveys/edit/adjust_the_theme_in_the: bccdafda8af5871513266f668b55d690
|
||||
environments/surveys/edit/all_are_true: 05d02c5afac857da530b73dcf18dd8e4
|
||||
environments/surveys/edit/all_other_answers_will_continue_to: 9a5d09eea42ff5fd1c18cc58a14dcabd
|
||||
environments/surveys/edit/allow_multi_select: 7b4b83f7a0205e2a0a8971671a69a174
|
||||
environments/surveys/edit/allow_multiple_files: dbd99f9d1026e4f7c5a5d03f71ba379d
|
||||
environments/surveys/edit/allow_users_to_select_more_than_one_image: d683e0b538d1366400292a771f3fbd08
|
||||
environments/surveys/edit/and_launch_surveys_in_your_website_or_app: a3edcdb4aea792a27d90aad1930f001a
|
||||
environments/surveys/edit/any_is_true: 32c9f3998984fd32a2b5bc53f2d97429
|
||||
environments/surveys/edit/animation: 66a18eacfb92fc9fc9db188d2dde4f81
|
||||
environments/surveys/edit/app_survey_description: bdfacfce478e97f70b700a1382dfa687
|
||||
environments/surveys/edit/assign: e80715ab64bf7cf463abb3a9fd1ad516
|
||||
@@ -1369,7 +1380,6 @@ checksums:
|
||||
environments/surveys/edit/follow_ups_modal_updated_successfull_toast: 61204fada3231f4f1fe3866e87e1130a
|
||||
environments/surveys/edit/follow_ups_new: 224c779d252b3e75086e4ed456ba2548
|
||||
environments/surveys/edit/follow_ups_upgrade_button_text: 4cd167527fc6cdb5b0bfc9b486b142a8
|
||||
environments/surveys/edit/form_styling: 1278a2db4257b5500474161133acc857
|
||||
environments/surveys/edit/formbricks_sdk_is_not_connected: 35165b0cac182a98408007a378cc677e
|
||||
environments/surveys/edit/four_points: b289628a6b8a6cd0f7d17a14ca6cd7bf
|
||||
environments/surveys/edit/heading: 79e9dfa461f38a239d34b9833ca103f1
|
||||
@@ -1484,6 +1494,7 @@ checksums:
|
||||
environments/surveys/edit/question_deleted: ecdeb22b81ae2d732656a7742c1eec7b
|
||||
environments/surveys/edit/question_duplicated: 3f02439fd0a8b818bc84c1b1b473898c
|
||||
environments/surveys/edit/question_id_updated: e8d94dbefcbad00c7464b3d1fb0ee81a
|
||||
environments/surveys/edit/question_number: 742636e9d2d5dcc7ee6ca1b3016bcee7
|
||||
environments/surveys/edit/question_used_in_logic_warning_text: ec78767a7cf335222d41b98cb5baa6be
|
||||
environments/surveys/edit/question_used_in_logic_warning_title: 4bb8528cdc3b8649c194487067737f6d
|
||||
environments/surveys/edit/question_used_in_quota: ceb5e88f6916e4863e589c6be030bb3b
|
||||
@@ -1540,7 +1551,7 @@ checksums:
|
||||
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
|
||||
environments/surveys/edit/response_options: 2988136d5248d7726583108992dcbaee
|
||||
environments/surveys/edit/roundness: 5a161c8f5f258defb57ed1d551737cc4
|
||||
environments/surveys/edit/roundness_description: bde131aa5674836416dcdf2ff517d899
|
||||
environments/surveys/edit/roundness_description: 03940a6871ae43efa4810cba7cadb74b
|
||||
environments/surveys/edit/row_used_in_logic_error: f89453ff1b6db77ad84af840fedd9813
|
||||
environments/surveys/edit/rows: 8f41f34e6ca28221cf1ebd948af4c151
|
||||
environments/surveys/edit/save_and_close: 6ede705b3f82f30269ff3054a5049e34
|
||||
@@ -1586,6 +1597,7 @@ checksums:
|
||||
environments/surveys/edit/survey_completed_subheading: db537c356c3ab6564d24de0d11a0fee2
|
||||
environments/surveys/edit/survey_display_settings: 8ed19e6a8e1376f7a1ba037d82c4ae11
|
||||
environments/surveys/edit/survey_placement: 083c10f257337f9648bf9d435b18ec2c
|
||||
environments/surveys/edit/survey_styling: 7f96d6563e934e65687b74374a33b1dc
|
||||
environments/surveys/edit/survey_trigger: f0c7014a684ca566698b87074fad5579
|
||||
environments/surveys/edit/switch_multi_language_on_to_get_started: cca0ef91ee49095da30cd1e3f26c406f
|
||||
environments/surveys/edit/target_block_not_found: 0a0c401017ab32364fec2fcbf815d832
|
||||
@@ -1672,9 +1684,9 @@ checksums:
|
||||
environments/surveys/edit/waiting_time_across_surveys: 6873c18d51830e2cadef67cce6a2c95c
|
||||
environments/surveys/edit/waiting_time_across_surveys_description: 6edafaeb3ccd8cadde81175776636c8e
|
||||
environments/surveys/edit/welcome_message: 986a434e3895c8ee0b267df95cc40051
|
||||
environments/surveys/edit/when: a40ad3eed1b75e76226290eeb9bb20cd
|
||||
environments/surveys/edit/without_a_filter_all_of_your_users_can_be_surveyed: 451990569c61f25d01044cc45b1ce122
|
||||
environments/surveys/edit/you_have_not_created_a_segment_yet: c6658bd1cee9c5c957c675db044708dd
|
||||
environments/surveys/edit/you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations: 04241177ba989ef4c1d8c01e1a7b8541
|
||||
environments/surveys/edit/your_description_here_recall_information_with: 60f73a3cc9bdb9afea2166a7db8fd618
|
||||
environments/surveys/edit/your_question_here_recall_information_with: 6395bd54f5167830c9d662ba403da167
|
||||
environments/surveys/edit/your_web_app: 07234bed03a33330dc50ae9fcf0174f3
|
||||
@@ -1920,6 +1932,7 @@ checksums:
|
||||
environments/surveys/summary/starts: 3153990a4ade414f501a7e63ab771362
|
||||
environments/surveys/summary/starts_tooltip: 0a7dd01320490dbbea923053fa1ccad6
|
||||
environments/surveys/summary/survey_reset_successfully: f53db36a28980ef4766215cf13f01e51
|
||||
environments/surveys/summary/survey_results: b7d86f636beaee2b4d5746bdda058d07
|
||||
environments/surveys/summary/this_month: 50845a38865204a97773c44dcd2ebb90
|
||||
environments/surveys/summary/this_quarter: 9c77d94783dff2269c069389122cd7bd
|
||||
environments/surveys/summary/this_year: 1e69651c2ac722f8ce138f43cf2e02f9
|
||||
@@ -2039,7 +2052,7 @@ checksums:
|
||||
environments/workspace/look/advanced_styling_field_description_size: a0d51c3ab7dc56320ecedc2b27917842
|
||||
environments/workspace/look/advanced_styling_field_description_size_description: ff880ea1beddd1b1ec7416d0b8a69cf3
|
||||
environments/workspace/look/advanced_styling_field_description_weight: 514680cc7202ad29835c1cbcde3def1c
|
||||
environments/workspace/look/advanced_styling_field_description_weight_description: 441ac8db1a32557813eb68fbfd759061
|
||||
environments/workspace/look/advanced_styling_field_description_weight_description: aa95bc81b5336a548e256bce49350683
|
||||
environments/workspace/look/advanced_styling_field_font_size: ca44d14429b2175a1b194793b4ab8f6b
|
||||
environments/workspace/look/advanced_styling_field_font_weight: bfef83778146cf40550df9650d8a07da
|
||||
environments/workspace/look/advanced_styling_field_headline_color: 4ccf3935ad90c88ad4add24f498673ce
|
||||
@@ -2053,7 +2066,7 @@ checksums:
|
||||
environments/workspace/look/advanced_styling_field_indicator_bg_description: 7eb3b54a8b331354ec95c0dc1545c620
|
||||
environments/workspace/look/advanced_styling_field_input_border_radius_description: 0007f1bb572b35d9a3720daeb7a55617
|
||||
environments/workspace/look/advanced_styling_field_input_font_size_description: 5311f95dcbd083623e35c98ea5374c3b
|
||||
environments/workspace/look/advanced_styling_field_input_height_description: e19ec0dc432478def0fd1199ad765e38
|
||||
environments/workspace/look/advanced_styling_field_input_height_description: bb7439d42ec3848a8fa9edb8b001b69a
|
||||
environments/workspace/look/advanced_styling_field_input_padding_x_description: 10e14296468321c13fda77fd1ba58dfd
|
||||
environments/workspace/look/advanced_styling_field_input_padding_y_description: 98b4aeff2940516d05ea61bdc1211d0d
|
||||
environments/workspace/look/advanced_styling_field_input_placeholder_opacity_description: f55a6700884d24014404e58876121ddf
|
||||
@@ -2062,6 +2075,8 @@ checksums:
|
||||
environments/workspace/look/advanced_styling_field_input_text_description: 460450df24ea0cc902710118a5000feb
|
||||
environments/workspace/look/advanced_styling_field_option_bg: 0ceaed10d99ed4ad83cb0934ab970174
|
||||
environments/workspace/look/advanced_styling_field_option_bg_description: 6cd6ccecbbb9f2f19439d7c682eb67c1
|
||||
environments/workspace/look/advanced_styling_field_option_border: aa478eb148515b6a2637fb144ff72028
|
||||
environments/workspace/look/advanced_styling_field_option_border_description: 8f75b740e8dcb7f6cfeff2e5d5ca7c92
|
||||
environments/workspace/look/advanced_styling_field_option_border_radius_description: 23f81c25b2681a7c9e2c4f2e7d2e0656
|
||||
environments/workspace/look/advanced_styling_field_option_font_size_description: 5430fd9b08819972f0a613bf3fa659da
|
||||
environments/workspace/look/advanced_styling_field_option_label: 2767a5db32742073a01aac16488e93dc
|
||||
@@ -2233,6 +2248,16 @@ checksums:
|
||||
templates/alignment_and_engagement_survey_question_4_headline: e36be56ce8aad1d0ca04939bea4e39b7
|
||||
templates/alignment_and_engagement_survey_question_4_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
|
||||
templates/back: f541015a827e37cb3b1234e56bc2aa3c
|
||||
templates/block_1: 5e1b4dce0cb70662441b663507a69454
|
||||
templates/block_2: f50d8aab8b44f168a2ab00526d4f9a2c
|
||||
templates/block_3: 78d84f8e4763a95710543c5368ce8a41
|
||||
templates/block_4: 2c346374f245a6821940c061b855ac69
|
||||
templates/block_5: 975abfc66e8e377478ff691a040dda0b
|
||||
templates/block_6: 2bd10f1edb210243c5ab459c59e02d30
|
||||
templates/block_7: 13f0f680c09c96081e125123ad2f6786
|
||||
templates/block_8: 1be1b18e159e8c8d11d2fb1082ea5d98
|
||||
templates/block_9: 2da3894d05e4415fa043ba18d11d60e2
|
||||
templates/block_10: 09a42e99b34b45700e734730acfe37ed
|
||||
templates/book_interview: 1cc9c72d1c088b28e5dfa5ec7d7b78c4
|
||||
templates/build_product_roadmap_description: 6ca163ed3b0095cedcbc11822a0d502a
|
||||
templates/build_product_roadmap_name: 8c216b183c3539c0340ce87465a391cc
|
||||
@@ -2440,7 +2465,6 @@ checksums:
|
||||
templates/csat_survey_question_3_headline: 25974b7f1692cad41908fe305830b6c0
|
||||
templates/csat_survey_question_3_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
|
||||
templates/cta_description: bc94a2ddc965b286a8677b0642696c7e
|
||||
templates/custom_survey_block_1_name: 5e1b4dce0cb70662441b663507a69454
|
||||
templates/custom_survey_description: 0492afdea2ef1bd683eaf48a2bad2caa
|
||||
templates/custom_survey_name: 6fc756927ca9ea22c26368cccd64a67e
|
||||
templates/custom_survey_question_1_headline: 0abf9d41e0b5c5567c3833fd63048398
|
||||
@@ -2843,6 +2867,9 @@ checksums:
|
||||
templates/preview_survey_question_2_choice_2_label: 1af148222f327f28cf0db6513de5989e
|
||||
templates/preview_survey_question_2_headline: 5cfb173d156555227fbc2c97ad921e72
|
||||
templates/preview_survey_question_2_subheader: 2e652d8acd68d072e5a0ae686c4011c0
|
||||
templates/preview_survey_question_open_text_headline: a9509a47e0456ae98ec3ddac3d6fad2c
|
||||
templates/preview_survey_question_open_text_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
|
||||
templates/preview_survey_question_open_text_subheader: 3c7bf09f3f17b02bc2fbbbdb347a5830
|
||||
templates/preview_survey_welcome_card_headline: 8778dc41547a2778d0f9482da989fc00
|
||||
templates/prioritize_features_description: 1eae41fad0e3947f803d8539081e59ec
|
||||
templates/prioritize_features_name: 4ca59ff1f9c319aaa68c3106d820fd6a
|
||||
|
||||
@@ -63,7 +63,8 @@ export const INVITE_DISABLED = env.INVITE_DISABLED === "1";
|
||||
|
||||
export const SLACK_CLIENT_SECRET = env.SLACK_CLIENT_SECRET;
|
||||
export const SLACK_CLIENT_ID = env.SLACK_CLIENT_ID;
|
||||
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read`;
|
||||
export const SLACK_REDIRECT_URI = `${WEBAPP_URL}/api/v1/integrations/slack/callback`;
|
||||
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read&redirect_uri=${SLACK_REDIRECT_URI}`;
|
||||
|
||||
export const GOOGLE_SHEETS_CLIENT_ID = env.GOOGLE_SHEETS_CLIENT_ID;
|
||||
export const GOOGLE_SHEETS_CLIENT_SECRET = env.GOOGLE_SHEETS_CLIENT_SECRET;
|
||||
|
||||
@@ -1,44 +1,66 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
// Mock constants module
|
||||
const envMock = {
|
||||
env: {
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
PUBLIC_URL: undefined as string | undefined,
|
||||
},
|
||||
WEBAPP_URL: undefined as string | undefined,
|
||||
VERCEL_URL: undefined as string | undefined,
|
||||
PUBLIC_URL: undefined as string | undefined,
|
||||
};
|
||||
|
||||
vi.mock("@/lib/env", () => envMock);
|
||||
vi.mock("./env", () => ({
|
||||
env: envMock,
|
||||
}));
|
||||
|
||||
const loadGetPublicDomain = async () => {
|
||||
vi.resetModules();
|
||||
const { getPublicDomain } = await import("./getPublicUrl");
|
||||
return getPublicDomain;
|
||||
};
|
||||
|
||||
describe("getPublicDomain", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
envMock.WEBAPP_URL = undefined;
|
||||
envMock.VERCEL_URL = undefined;
|
||||
envMock.PUBLIC_URL = undefined;
|
||||
});
|
||||
|
||||
test("should return WEBAPP_URL when PUBLIC_URL is not set", async () => {
|
||||
const { getPublicDomain } = await import("./getPublicUrl");
|
||||
const domain = getPublicDomain();
|
||||
expect(domain).toBe("http://localhost:3000");
|
||||
test("returns trimmed WEBAPP_URL when configured", async () => {
|
||||
envMock.WEBAPP_URL = " https://app.formbricks.com ";
|
||||
|
||||
const getPublicDomain = await loadGetPublicDomain();
|
||||
|
||||
expect(getPublicDomain()).toBe("https://app.formbricks.com");
|
||||
});
|
||||
|
||||
test("should return PUBLIC_URL when it is set", async () => {
|
||||
envMock.env.PUBLIC_URL = "https://surveys.example.com";
|
||||
const { getPublicDomain } = await import("./getPublicUrl");
|
||||
const domain = getPublicDomain();
|
||||
expect(domain).toBe("https://surveys.example.com");
|
||||
test("falls back to VERCEL_URL when WEBAPP_URL is empty", async () => {
|
||||
envMock.WEBAPP_URL = " ";
|
||||
envMock.VERCEL_URL = "preview.formbricks.com";
|
||||
|
||||
const getPublicDomain = await loadGetPublicDomain();
|
||||
|
||||
expect(getPublicDomain()).toBe("https://preview.formbricks.com");
|
||||
});
|
||||
|
||||
test("should handle empty string PUBLIC_URL by returning WEBAPP_URL", async () => {
|
||||
envMock.env.PUBLIC_URL = "";
|
||||
const { getPublicDomain } = await import("./getPublicUrl");
|
||||
const domain = getPublicDomain();
|
||||
expect(domain).toBe("http://localhost:3000");
|
||||
test("falls back to localhost when WEBAPP_URL and VERCEL_URL are not set", async () => {
|
||||
const getPublicDomain = await loadGetPublicDomain();
|
||||
|
||||
expect(getPublicDomain()).toBe("http://localhost:3000");
|
||||
});
|
||||
|
||||
test("should handle undefined PUBLIC_URL by returning WEBAPP_URL", async () => {
|
||||
envMock.env.PUBLIC_URL = undefined;
|
||||
const { getPublicDomain } = await import("./getPublicUrl");
|
||||
const domain = getPublicDomain();
|
||||
expect(domain).toBe("http://localhost:3000");
|
||||
test("returns PUBLIC_URL when set", async () => {
|
||||
envMock.WEBAPP_URL = "https://app.formbricks.com";
|
||||
envMock.PUBLIC_URL = "https://surveys.formbricks.com";
|
||||
|
||||
const getPublicDomain = await loadGetPublicDomain();
|
||||
|
||||
expect(getPublicDomain()).toBe("https://surveys.formbricks.com");
|
||||
});
|
||||
|
||||
test("falls back to WEBAPP_URL when PUBLIC_URL is empty", async () => {
|
||||
envMock.WEBAPP_URL = "https://app.formbricks.com";
|
||||
envMock.PUBLIC_URL = " ";
|
||||
|
||||
const getPublicDomain = await loadGetPublicDomain();
|
||||
|
||||
expect(getPublicDomain()).toBe("https://app.formbricks.com");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import "server-only";
|
||||
import { env } from "./env";
|
||||
|
||||
const WEBAPP_URL =
|
||||
env.WEBAPP_URL ?? (env.VERCEL_URL ? `https://${env.VERCEL_URL}` : "") ?? "http://localhost:3000";
|
||||
const configuredWebappUrl = env.WEBAPP_URL?.trim() ?? "";
|
||||
const WEBAPP_URL = (() => {
|
||||
if (configuredWebappUrl !== "") {
|
||||
return configuredWebappUrl;
|
||||
}
|
||||
|
||||
if (env.VERCEL_URL) {
|
||||
return `https://${env.VERCEL_URL}`;
|
||||
}
|
||||
|
||||
return "http://localhost:3000";
|
||||
})();
|
||||
|
||||
/**
|
||||
* Returns the public domain URL
|
||||
|
||||
@@ -130,84 +130,102 @@ export const appLanguages = [
|
||||
code: "de-DE",
|
||||
label: {
|
||||
"en-US": "German",
|
||||
native: "Deutsch",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "en-US",
|
||||
label: {
|
||||
"en-US": "English (US)",
|
||||
native: "English (US)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "es-ES",
|
||||
label: {
|
||||
"en-US": "Spanish",
|
||||
native: "Español",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "fr-FR",
|
||||
label: {
|
||||
"en-US": "French",
|
||||
native: "Français",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "hu-HU",
|
||||
label: {
|
||||
"en-US": "Hungarian",
|
||||
native: "Magyar",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "ja-JP",
|
||||
label: {
|
||||
"en-US": "Japanese",
|
||||
native: "日本語",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "nl-NL",
|
||||
label: {
|
||||
"en-US": "Dutch",
|
||||
native: "Nederlands",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "pt-BR",
|
||||
label: {
|
||||
"en-US": "Portuguese (Brazil)",
|
||||
native: "Português (Brasil)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "pt-PT",
|
||||
label: {
|
||||
"en-US": "Portuguese (Portugal)",
|
||||
native: "Português (Portugal)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "ro-RO",
|
||||
label: {
|
||||
"en-US": "Romanian",
|
||||
native: "Română",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "ru-RU",
|
||||
label: {
|
||||
"en-US": "Russian",
|
||||
native: "Русский",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "sv-SE",
|
||||
label: {
|
||||
"en-US": "Swedish",
|
||||
native: "Svenska",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "zh-Hans-CN",
|
||||
label: {
|
||||
"en-US": "Chinese (Simplified)",
|
||||
native: "简体中文",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "zh-Hant-TW",
|
||||
label: {
|
||||
"en-US": "Chinese (Traditional)",
|
||||
native: "繁體中文",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const sortedAppLanguages = [...appLanguages].sort((a, b) =>
|
||||
a.label["en-US"].localeCompare(b.label["en-US"])
|
||||
);
|
||||
|
||||
@@ -397,7 +397,6 @@ export const getResponseDownloadFile = async (
|
||||
"Survey ID",
|
||||
"Formbricks ID (internal)",
|
||||
"User ID",
|
||||
"Notes",
|
||||
"Tags",
|
||||
...metaDataFields,
|
||||
...elements.flat(),
|
||||
|
||||
@@ -60,6 +60,7 @@ export const getSuggestedColors = (brandColor: string = DEFAULT_BRAND_COLOR) =>
|
||||
// Options (Radio / Checkbox)
|
||||
"optionBgColor.light": inputBg,
|
||||
"optionLabelColor.light": questionColor,
|
||||
"optionBorderColor.light": inputBorder,
|
||||
|
||||
// Card
|
||||
"cardBackgroundColor.light": cardBg,
|
||||
@@ -138,6 +139,7 @@ export const STYLE_DEFAULTS: TProjectStyling = {
|
||||
// Options
|
||||
optionBgColor: { light: _colors["optionBgColor.light"] },
|
||||
optionLabelColor: { light: _colors["optionLabelColor.light"] },
|
||||
optionBorderColor: { light: _colors["optionBorderColor.light"] },
|
||||
optionBorderRadius: 8,
|
||||
optionPaddingX: 16,
|
||||
optionPaddingY: 16,
|
||||
@@ -169,6 +171,7 @@ export const deriveNewFieldsFromLegacy = (saved: Record<string, unknown>): Recor
|
||||
const q = light("questionColor");
|
||||
const b = light("brandColor");
|
||||
const i = light("inputColor");
|
||||
const inputBorder = light("inputBorderColor");
|
||||
|
||||
return {
|
||||
...(q && !saved.elementHeadlineColor && { elementHeadlineColor: { light: q } }),
|
||||
@@ -179,9 +182,9 @@ export const deriveNewFieldsFromLegacy = (saved: Record<string, unknown>): Recor
|
||||
...(b && !saved.buttonBgColor && { buttonBgColor: { light: b } }),
|
||||
...(b && !saved.buttonTextColor && { buttonTextColor: { light: isLight(b) ? "#0f172a" : "#ffffff" } }),
|
||||
...(i && !saved.optionBgColor && { optionBgColor: { light: i } }),
|
||||
...(inputBorder && !saved.optionBorderColor && { optionBorderColor: { light: inputBorder } }),
|
||||
...(b && !saved.progressIndicatorBgColor && { progressIndicatorBgColor: { light: b } }),
|
||||
...(b &&
|
||||
!saved.progressTrackBgColor && { progressTrackBgColor: { light: mixColor(b, "#ffffff", 0.8) } }),
|
||||
...(b && !saved.progressTrackBgColor && { progressTrackBgColor: { light: mixColor(b, "#ffffff", 0.8) } }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -211,6 +214,7 @@ export const buildStylingFromBrandColor = (brandColor: string = DEFAULT_BRAND_CO
|
||||
inputTextColor: { light: colors["inputTextColor.light"] },
|
||||
optionBgColor: { light: colors["optionBgColor.light"] },
|
||||
optionLabelColor: { light: colors["optionLabelColor.light"] },
|
||||
optionBorderColor: { light: colors["optionBorderColor.light"] },
|
||||
cardBackgroundColor: { light: colors["cardBackgroundColor.light"] },
|
||||
cardBorderColor: { light: colors["cardBorderColor.light"] },
|
||||
highlightBorderColor: { light: colors["highlightBorderColor.light"] },
|
||||
|
||||
@@ -508,6 +508,7 @@ export const updateSurveyInternal = async (
|
||||
newFollowUps.length > 0
|
||||
? {
|
||||
data: newFollowUps.map((followUp) => ({
|
||||
id: followUp.id,
|
||||
name: followUp.name,
|
||||
trigger: followUp.trigger,
|
||||
action: followUp.action,
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
import dns from "node:dns";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { validateWebhookUrl } from "./validate-webhook-url";
|
||||
|
||||
vi.mock("node:dns", () => ({
|
||||
default: {
|
||||
resolve: vi.fn(),
|
||||
resolve6: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockResolve = vi.mocked(dns.resolve);
|
||||
const mockResolve6 = vi.mocked(dns.resolve6);
|
||||
|
||||
type DnsCallback = (err: NodeJS.ErrnoException | null, addresses: string[]) => void;
|
||||
|
||||
const setupDnsResolution = (ipv4: string[] | null, ipv6: string[] | null = null): void => {
|
||||
// dns.resolve/resolve6 have overloaded signatures; we only mock the (hostname, callback) form
|
||||
mockResolve.mockImplementation(((_hostname: string, callback: DnsCallback) => {
|
||||
if (ipv4) {
|
||||
callback(null, ipv4);
|
||||
} else {
|
||||
callback(new Error("ENOTFOUND"), []);
|
||||
}
|
||||
}) as never);
|
||||
|
||||
mockResolve6.mockImplementation(((_hostname: string, callback: DnsCallback) => {
|
||||
if (ipv6) {
|
||||
callback(null, ipv6);
|
||||
} else {
|
||||
callback(new Error("ENOTFOUND"), []);
|
||||
}
|
||||
}) as never);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("validateWebhookUrl", () => {
|
||||
describe("valid public URLs", () => {
|
||||
test("accepts HTTPS URL resolving to a public IPv4 address", async () => {
|
||||
setupDnsResolution(["93.184.216.34"]);
|
||||
await expect(validateWebhookUrl("https://example.com/webhook")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("accepts HTTP URL resolving to a public IPv4 address", async () => {
|
||||
setupDnsResolution(["93.184.216.34"]);
|
||||
await expect(validateWebhookUrl("http://example.com/webhook")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("accepts URL with port and path segments", async () => {
|
||||
setupDnsResolution(["93.184.216.34"]);
|
||||
await expect(validateWebhookUrl("https://example.com:8443/api/v1/webhook")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("accepts URL resolving to a public IPv6 address", async () => {
|
||||
setupDnsResolution(null, ["2606:2800:220:1:248:1893:25c8:1946"]);
|
||||
await expect(validateWebhookUrl("https://example.com/webhook")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("accepts a public IPv4 address as hostname", async () => {
|
||||
await expect(validateWebhookUrl("https://93.184.216.34/webhook")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("URL format validation", () => {
|
||||
test("rejects a completely malformed string", async () => {
|
||||
await expect(validateWebhookUrl("not-a-url")).rejects.toThrow("Invalid webhook URL format");
|
||||
});
|
||||
|
||||
test("rejects an empty string", async () => {
|
||||
await expect(validateWebhookUrl("")).rejects.toThrow("Invalid webhook URL format");
|
||||
});
|
||||
});
|
||||
|
||||
describe("protocol validation", () => {
|
||||
test("rejects FTP protocol", async () => {
|
||||
await expect(validateWebhookUrl("ftp://example.com/file")).rejects.toThrow(
|
||||
"Webhook URL must use HTTPS or HTTP protocol"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects file:// protocol", async () => {
|
||||
await expect(validateWebhookUrl("file:///etc/passwd")).rejects.toThrow(
|
||||
"Webhook URL must use HTTPS or HTTP protocol"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects javascript: protocol", async () => {
|
||||
await expect(validateWebhookUrl("javascript:alert(1)")).rejects.toThrow(
|
||||
"Webhook URL must use HTTPS or HTTP protocol"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("blocked hostname validation", () => {
|
||||
test("rejects localhost", async () => {
|
||||
await expect(validateWebhookUrl("http://localhost/admin")).rejects.toThrow(
|
||||
"Webhook URL must not point to localhost or internal services"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects localhost.localdomain", async () => {
|
||||
await expect(validateWebhookUrl("https://localhost.localdomain/path")).rejects.toThrow(
|
||||
"Webhook URL must not point to localhost or internal services"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects metadata.google.internal", async () => {
|
||||
await expect(validateWebhookUrl("http://metadata.google.internal/computeMetadata/v1/")).rejects.toThrow(
|
||||
"Webhook URL must not point to localhost or internal services"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("private IPv4 literal blocking", () => {
|
||||
test("rejects 127.0.0.1 (loopback)", async () => {
|
||||
await expect(validateWebhookUrl("http://127.0.0.1/metadata")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 127.0.0.53 (loopback range)", async () => {
|
||||
await expect(validateWebhookUrl("http://127.0.0.53/")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 10.0.0.1 (Class A private)", async () => {
|
||||
await expect(validateWebhookUrl("http://10.0.0.1/internal")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 172.16.0.1 (Class B private)", async () => {
|
||||
await expect(validateWebhookUrl("http://172.16.0.1/internal")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 172.31.255.255 (Class B private upper bound)", async () => {
|
||||
await expect(validateWebhookUrl("http://172.31.255.255/internal")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 192.168.1.1 (Class C private)", async () => {
|
||||
await expect(validateWebhookUrl("http://192.168.1.1/internal")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 169.254.169.254 (AWS/GCP/Azure metadata endpoint)", async () => {
|
||||
await expect(validateWebhookUrl("http://169.254.169.254/latest/meta-data/")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 0.0.0.0 ('this' network)", async () => {
|
||||
await expect(validateWebhookUrl("http://0.0.0.0/")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects 100.64.0.1 (CGNAT / shared address space)", async () => {
|
||||
await expect(validateWebhookUrl("http://100.64.0.1/")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DNS resolution with private IP results", () => {
|
||||
test("rejects hostname resolving to loopback address", async () => {
|
||||
setupDnsResolution(["127.0.0.1"]);
|
||||
await expect(validateWebhookUrl("https://evil.com/steal")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to cloud metadata endpoint IP", async () => {
|
||||
setupDnsResolution(["169.254.169.254"]);
|
||||
await expect(validateWebhookUrl("https://attacker.com/ssrf")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to Class A private network", async () => {
|
||||
setupDnsResolution(["10.0.0.5"]);
|
||||
await expect(validateWebhookUrl("https://internal.service/api")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to Class C private network", async () => {
|
||||
setupDnsResolution(["192.168.0.1"]);
|
||||
await expect(validateWebhookUrl("https://sneaky.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to IPv6 loopback", async () => {
|
||||
setupDnsResolution(null, ["::1"]);
|
||||
await expect(validateWebhookUrl("https://sneaky.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to IPv6 link-local", async () => {
|
||||
setupDnsResolution(null, ["fe80::1"]);
|
||||
await expect(validateWebhookUrl("https://link-local.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to IPv6 unique local address", async () => {
|
||||
setupDnsResolution(null, ["fd12:3456:789a::1"]);
|
||||
await expect(validateWebhookUrl("https://ula.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to IPv4-mapped IPv6 private address (dotted)", async () => {
|
||||
setupDnsResolution(null, ["::ffff:192.168.1.1"]);
|
||||
await expect(validateWebhookUrl("https://mapped.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hostname resolving to IPv4-mapped IPv6 private address (hex-encoded)", async () => {
|
||||
setupDnsResolution(null, ["::ffff:c0a8:0101"]); // 192.168.1.1 in hex
|
||||
await expect(validateWebhookUrl("https://hex-mapped.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hex-encoded IPv4-mapped loopback (::ffff:7f00:0001)", async () => {
|
||||
setupDnsResolution(null, ["::ffff:7f00:0001"]); // 127.0.0.1 in hex
|
||||
await expect(validateWebhookUrl("https://hex-loopback.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects hex-encoded IPv4-mapped metadata endpoint (::ffff:a9fe:a9fe)", async () => {
|
||||
setupDnsResolution(null, ["::ffff:a9fe:a9fe"]); // 169.254.169.254 in hex
|
||||
await expect(validateWebhookUrl("https://hex-metadata.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts hex-encoded IPv4-mapped public address", async () => {
|
||||
setupDnsResolution(null, ["::ffff:5db8:d822"]); // 93.184.216.34 in hex
|
||||
await expect(validateWebhookUrl("https://hex-public.example.com/webhook")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("rejects when any resolved IP is private (mixed public + private)", async () => {
|
||||
setupDnsResolution(["93.184.216.34", "192.168.1.1"]);
|
||||
await expect(validateWebhookUrl("https://dual.example.com/webhook")).rejects.toThrow(
|
||||
"Webhook URL must not point to private or internal IP addresses"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects unresolvable hostname", async () => {
|
||||
setupDnsResolution(null, null);
|
||||
await expect(validateWebhookUrl("https://nonexistent.invalid/path")).rejects.toThrow(
|
||||
"Could not resolve webhook URL hostname"
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects with timeout error when DNS resolution hangs", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
mockResolve.mockImplementation((() => {
|
||||
// never calls callback — simulates a hanging DNS server
|
||||
}) as never);
|
||||
|
||||
const promise = validateWebhookUrl("https://slow-dns.example.com/webhook");
|
||||
|
||||
const assertion = expect(promise).rejects.toThrow(
|
||||
"DNS resolution timed out for webhook URL hostname: slow-dns.example.com"
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
await assertion;
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("error type", () => {
|
||||
test("throws InvalidInputError (not generic Error)", async () => {
|
||||
await expect(validateWebhookUrl("http://127.0.0.1/")).rejects.toMatchObject({
|
||||
name: "InvalidInputError",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import "server-only";
|
||||
import dns from "node:dns";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
|
||||
const BLOCKED_HOSTNAMES = new Set([
|
||||
"localhost",
|
||||
"localhost.localdomain",
|
||||
"ip6-localhost",
|
||||
"ip6-loopback",
|
||||
"metadata.google.internal",
|
||||
]);
|
||||
|
||||
const PRIVATE_IPV4_PATTERNS: RegExp[] = [
|
||||
/^127\./, // 127.0.0.0/8 – Loopback
|
||||
/^10\./, // 10.0.0.0/8 – Class A private
|
||||
/^172\.(1[6-9]|2\d|3[01])\./, // 172.16.0.0/12 – Class B private
|
||||
/^192\.168\./, // 192.168.0.0/16 – Class C private
|
||||
/^169\.254\./, // 169.254.0.0/16 – Link-local (AWS/GCP/Azure metadata)
|
||||
/^0\./, // 0.0.0.0/8 – "This" network
|
||||
/^100\.(6[4-9]|[7-9]\d|1[0-2]\d)\./, // 100.64.0.0/10 – Shared address space (RFC 6598)
|
||||
/^192\.0\.0\./, // 192.0.0.0/24 – IETF protocol assignments
|
||||
/^192\.0\.2\./, // 192.0.2.0/24 – TEST-NET-1 (documentation)
|
||||
/^198\.51\.100\./, // 198.51.100.0/24 – TEST-NET-2 (documentation)
|
||||
/^203\.0\.113\./, // 203.0.113.0/24 – TEST-NET-3 (documentation)
|
||||
/^198\.1[89]\./, // 198.18.0.0/15 – Benchmarking
|
||||
/^224\./, // 224.0.0.0/4 – Multicast
|
||||
/^240\./, // 240.0.0.0/4 – Reserved for future use
|
||||
/^255\.255\.255\.255$/, // Limited broadcast
|
||||
];
|
||||
|
||||
const PRIVATE_IPV6_PREFIXES = [
|
||||
"::1", // Loopback
|
||||
"fe80:", // Link-local
|
||||
"fc", // Unique local address (ULA, fc00::/7 — covers fc00:: through fdff::)
|
||||
"fd", // Unique local address (ULA, fc00::/7 — covers fc00:: through fdff::)
|
||||
];
|
||||
|
||||
const isPrivateIPv4 = (ip: string): boolean => {
|
||||
return PRIVATE_IPV4_PATTERNS.some((pattern) => pattern.test(ip));
|
||||
};
|
||||
|
||||
const hexMappedToIPv4 = (hexPart: string): string | null => {
|
||||
const groups = hexPart.split(":");
|
||||
if (groups.length !== 2) return null;
|
||||
const high = Number.parseInt(groups[0], 16);
|
||||
const low = Number.parseInt(groups[1], 16);
|
||||
if (Number.isNaN(high) || Number.isNaN(low) || high > 0xffff || low > 0xffff) return null;
|
||||
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
||||
};
|
||||
|
||||
const isIPv4Mapped = (normalized: string): boolean => {
|
||||
if (!normalized.startsWith("::ffff:")) return false;
|
||||
const suffix = normalized.slice(7); // strip "::ffff:"
|
||||
|
||||
if (suffix.includes(".")) {
|
||||
return isPrivateIPv4(suffix);
|
||||
}
|
||||
const dotted = hexMappedToIPv4(suffix);
|
||||
return dotted !== null && isPrivateIPv4(dotted);
|
||||
};
|
||||
|
||||
const isPrivateIPv6 = (ip: string): boolean => {
|
||||
const normalized = ip.toLowerCase();
|
||||
if (normalized === "::") return true;
|
||||
if (isIPv4Mapped(normalized)) return true;
|
||||
return PRIVATE_IPV6_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
||||
};
|
||||
|
||||
const isPrivateIP = (ip: string): boolean => {
|
||||
return isPrivateIPv4(ip) || isPrivateIPv6(ip);
|
||||
};
|
||||
|
||||
const DNS_TIMEOUT_MS = 3000;
|
||||
|
||||
const resolveHostnameToIPs = (hostname: string): Promise<string[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
const settle = <T>(fn: (value: T) => void, value: T): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
fn(value);
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
settle(reject, new Error(`DNS resolution timed out for hostname: ${hostname}`));
|
||||
}, DNS_TIMEOUT_MS);
|
||||
|
||||
dns.resolve(hostname, (errV4, ipv4Addresses) => {
|
||||
const ipv4 = errV4 ? [] : ipv4Addresses;
|
||||
|
||||
dns.resolve6(hostname, (errV6, ipv6Addresses) => {
|
||||
const ipv6 = errV6 ? [] : ipv6Addresses;
|
||||
const allAddresses = [...ipv4, ...ipv6];
|
||||
|
||||
if (allAddresses.length === 0) {
|
||||
settle(reject, new Error(`DNS resolution failed for hostname: ${hostname}`));
|
||||
} else {
|
||||
settle(resolve, allAddresses);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const stripIPv6Brackets = (hostname: string): string => {
|
||||
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
||||
return hostname.slice(1, -1);
|
||||
}
|
||||
return hostname;
|
||||
};
|
||||
|
||||
const IPV4_LITERAL = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
||||
|
||||
/**
|
||||
* Validates a webhook URL to prevent Server-Side Request Forgery (SSRF).
|
||||
*
|
||||
* Checks performed:
|
||||
* 1. URL must be well-formed
|
||||
* 2. Protocol must be HTTPS or HTTP
|
||||
* 3. Hostname must not be a known internal name (localhost, metadata endpoints)
|
||||
* 4. IP literal hostnames are checked directly against private ranges
|
||||
* 5. Domain hostnames are resolved via DNS; all resulting IPs must be public
|
||||
*
|
||||
* @throws {InvalidInputError} when the URL fails any validation check
|
||||
*/
|
||||
export const validateWebhookUrl = async (url: string): Promise<void> => {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch {
|
||||
throw new InvalidInputError("Invalid webhook URL format");
|
||||
}
|
||||
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
||||
throw new InvalidInputError("Webhook URL must use HTTPS or HTTP protocol");
|
||||
}
|
||||
|
||||
const hostname = parsed.hostname;
|
||||
|
||||
if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) {
|
||||
throw new InvalidInputError("Webhook URL must not point to localhost or internal services");
|
||||
}
|
||||
|
||||
// Direct IP literal — validate without DNS resolution
|
||||
const isIPv4Literal = IPV4_LITERAL.test(hostname);
|
||||
const isIPv6Literal = hostname.startsWith("[");
|
||||
|
||||
if (isIPv4Literal || isIPv6Literal) {
|
||||
const ip = isIPv6Literal ? stripIPv6Brackets(hostname) : hostname;
|
||||
if (isPrivateIP(ip)) {
|
||||
throw new InvalidInputError("Webhook URL must not point to private or internal IP addresses");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Domain name — resolve DNS and validate every resolved IP
|
||||
let resolvedIPs: string[];
|
||||
try {
|
||||
resolvedIPs = await resolveHostnameToIPs(hostname);
|
||||
} catch (error) {
|
||||
const isTimeout = error instanceof Error && error.message.includes("timed out");
|
||||
throw new InvalidInputError(
|
||||
isTimeout
|
||||
? `DNS resolution timed out for webhook URL hostname: ${hostname}`
|
||||
: `Could not resolve webhook URL hostname: ${hostname}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const ip of resolvedIPs) {
|
||||
if (isPrivateIP(ip)) {
|
||||
throw new InvalidInputError("Webhook URL must not point to private or internal IP addresses");
|
||||
}
|
||||
}
|
||||
};
|
||||
+39
-13
@@ -175,9 +175,12 @@
|
||||
"copy": "Kopieren",
|
||||
"copy_code": "Code kopieren",
|
||||
"copy_link": "Link kopieren",
|
||||
"count_attributes": "{value, plural, one {{value} Attribut} other {{value} Attribute}}",
|
||||
"count_contacts": "{value, plural, one {{value} Kontakt} other {{value} Kontakte}}",
|
||||
"count_responses": "{value, plural, one {{value} Antwort} other {{value} Antworten}}",
|
||||
"count_attributes": "{count, plural, one {{count} Attribut} other {{count} Attribute}}",
|
||||
"count_contacts": "{count, plural, one {{count} Kontakt} other {{count} Kontakte}}",
|
||||
"count_members": "{count, plural, one {{count} Mitglied} other {{count} Mitglieder}}",
|
||||
"count_questions": "{count, plural, one {{count} Frage} other {{count} Fragen}}",
|
||||
"count_responses": "{count, plural, one {{count} Antwort} other {{count} Antworten}}",
|
||||
"count_selections": "{count, plural, one {{count} Auswahl} other {{count} Auswahlen}}",
|
||||
"create_new_organization": "Neue Organisation erstellen",
|
||||
"create_segment": "Segment erstellen",
|
||||
"create_survey": "Umfrage erstellen",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "Tage",
|
||||
"default": "Standard",
|
||||
"delete": "Löschen",
|
||||
"delete_what": "{deleteWhat} löschen",
|
||||
"description": "Beschreibung",
|
||||
"dev_env": "Entwicklungsumgebung",
|
||||
"development": "Entwicklung",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Herunterladen",
|
||||
"draft": "Entwurf",
|
||||
"duplicate": "Duplikat",
|
||||
"duplicate_copy": "(Kopie)",
|
||||
"duplicate_copy_number": "(Kopie {copyNumber})",
|
||||
"e_commerce": "E-Commerce",
|
||||
"edit": "Bearbeiten",
|
||||
"email": "E-Mail",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Projekte konnten nicht geladen werden",
|
||||
"filter": "Filter",
|
||||
"finish": "Fertigstellen",
|
||||
"first_name": "Vorname",
|
||||
"follow_these": "Folge diesen",
|
||||
"formbricks_version": "Formbricks Version",
|
||||
"full_name": "Name",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Verstecktes Feld",
|
||||
"hidden_fields": "Versteckte Felder",
|
||||
"hide_column": "Spalte ausblenden",
|
||||
"id": "ID",
|
||||
"image": "Bild",
|
||||
"images": "Bilder",
|
||||
"import": "Importieren",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Schlüssel",
|
||||
"label": "Bezeichnung",
|
||||
"language": "Sprache",
|
||||
"last_name": "Nachname",
|
||||
"learn_more": "Mehr erfahren",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Helle Überlagerung",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Darstellung",
|
||||
"manage": "Verwalten",
|
||||
"marketing": "Marketing",
|
||||
"member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"members_and_teams": "Mitglieder & Teams",
|
||||
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Nach unten bewegen",
|
||||
"move_up": "Nach oben bewegen",
|
||||
"multiple_languages": "Mehrsprachigkeit",
|
||||
"my_product": "mein Produkt",
|
||||
"name": "Name",
|
||||
"new": "Neu",
|
||||
"new_version_available": "Formbricks {version} ist da. Jetzt aktualisieren!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Teams auswählen",
|
||||
"selected": "Ausgewählt",
|
||||
"selected_questions": "Ausgewählte Fragen",
|
||||
"selection": "Auswahl",
|
||||
"selections": "Auswahlen",
|
||||
"send_test_email": "Test-E-Mail senden",
|
||||
"session_not_found": "Sitzung nicht gefunden",
|
||||
"settings": "Einstellungen",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Hey {userName}",
|
||||
"email_customization_preview_email_text": "Dies ist eine E-Mail-Vorschau, um dir zu zeigen, welches Logo in den E-Mails gerendert wird.",
|
||||
"error_deleting_organization_please_try_again": "Fehler beim Löschen der Organisation. Bitte versuche es erneut.",
|
||||
"from_your_organization": "von deiner Organisation",
|
||||
"from_your_organization": "{memberName} aus Ihrer Organisation",
|
||||
"invitation_sent_once_more": "Einladung nochmal gesendet.",
|
||||
"invite_deleted_successfully": "Einladung erfolgreich gelöscht",
|
||||
"invite_expires_on": "Einladung läuft ab am {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Platzhalter hinzufügen, falls kein Wert zur Verfügung steht.",
|
||||
"add_hidden_field_id": "Verstecktes Feld ID hinzufügen",
|
||||
"add_highlight_border": "Rahmen hinzufügen",
|
||||
"add_highlight_border_description": "Gilt nur für In-Product-Umfragen.",
|
||||
"add_logic": "Logik hinzufügen",
|
||||
"add_none_of_the_above": "Füge \"Keine der oben genannten Optionen\" hinzu",
|
||||
"add_option": "Option hinzufügen",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "'Umfrage geschlossen'-Nachricht anpassen",
|
||||
"adjust_survey_closed_message_description": "Ändere die Nachricht, die Besucher sehen, wenn die Umfrage geschlossen ist.",
|
||||
"adjust_the_theme_in_the": "Passe das Thema an in den",
|
||||
"all_are_true": "alle sind wahr",
|
||||
"all_other_answers_will_continue_to": "Alle anderen Antworten werden weiterhin",
|
||||
"allow_multi_select": "Mehrfachauswahl erlauben",
|
||||
"allow_multiple_files": "Mehrere Dateien zulassen",
|
||||
"allow_users_to_select_more_than_one_image": "Erlaube Nutzern, mehr als ein Bild auszuwählen",
|
||||
"and_launch_surveys_in_your_website_or_app": "und Umfragen auf deiner Website oder App starten.",
|
||||
"animation": "Animation",
|
||||
"any_is_true": "mindestens eine ist wahr",
|
||||
"app_survey_description": "Bette eine Umfrage in deine Web-App oder Website ein, um Antworten zu sammeln.",
|
||||
"assign": "Zuweisen =",
|
||||
"audience": "Publikum",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Nachverfolgung aktualisiert und wird gespeichert, sobald du die Umfrage speicherst.",
|
||||
"follow_ups_new": "Neues Follow-up",
|
||||
"follow_ups_upgrade_button_text": "Upgrade, um Follow-ups zu aktivieren",
|
||||
"form_styling": "Umfrage Styling",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDK ist nicht verbunden",
|
||||
"four_points": "4 Punkte",
|
||||
"heading": "Überschrift",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Option {choiceIndex}",
|
||||
"option_used_in_logic_error": "Diese Option wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
||||
"optional": "Optional",
|
||||
"options": "Optionen",
|
||||
"options": "Optionen*",
|
||||
"options_used_in_logic_bulk_error": "Die folgenden Optionen werden in der Logik verwendet: {questionIndexes}. Bitte entferne sie zuerst aus der Logik.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Styling für diese Umfrage überschreiben.",
|
||||
"overwrite_global_waiting_time": "Benutzerdefinierte Abkühlphase festlegen",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Frage gelöscht.",
|
||||
"question_duplicated": "Frage dupliziert.",
|
||||
"question_id_updated": "Frage-ID aktualisiert",
|
||||
"question_number": "Frage {number}",
|
||||
"question_used_in_logic_warning_text": "Elemente aus diesem Block werden in einer Logikregel verwendet. Möchten Sie ihn wirklich löschen?",
|
||||
"question_used_in_logic_warning_title": "Logikinkonsistenz",
|
||||
"question_used_in_quota": "Diese Frage wird in der “{quotaName}” Quote verwendet",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.",
|
||||
"response_options": "Antwortoptionen",
|
||||
"roundness": "Rundheit",
|
||||
"roundness_description": "Steuert, wie abgerundet die Kartenecken sind.",
|
||||
"roundness_description": "Steuert, wie abgerundet die Ecken sind.",
|
||||
"row_used_in_logic_error": "Diese Zeile wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
||||
"rows": "Zeilen",
|
||||
"save_and_close": "Speichern & Schließen",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen",
|
||||
"survey_display_settings": "Einstellungen zur Anzeige der Umfrage",
|
||||
"survey_placement": "Platzierung der Umfrage",
|
||||
"survey_styling": "Umfrage Styling",
|
||||
"survey_trigger": "Auslöser der Umfrage",
|
||||
"switch_multi_language_on_to_get_started": "Aktiviere Mehrsprachigkeit, um loszulegen 👉",
|
||||
"target_block_not_found": "Zielblock nicht gefunden",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Abkühlphase (umfrageübergreifend)",
|
||||
"waiting_time_across_surveys_description": "Um Umfragemüdigkeit zu vermeiden, wähle aus, wie diese Umfrage mit der workspace-weiten Abkühlphase interagiert.",
|
||||
"welcome_message": "Willkommensnachricht",
|
||||
"when": "Wenn",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Ohne Filter können alle deine Nutzer befragt werden.",
|
||||
"you_have_not_created_a_segment_yet": "Du hast noch keinen Segment erstellt.",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Sie müssen zwei oder mehr Sprachen in Ihrem Workspace eingerichtet haben, um mit Übersetzungen zu arbeiten.",
|
||||
"your_description_here_recall_information_with": "Deine Beschreibung hier. Informationen abrufen mit @",
|
||||
"your_question_here_recall_information_with": "Deine Frage hier. Informationen abrufen mit @",
|
||||
"your_web_app": "Deine Web-App",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Startet",
|
||||
"starts_tooltip": "So oft wurde die Umfrage gestartet.",
|
||||
"survey_reset_successfully": "Umfrage erfolgreich zurückgesetzt! {responseCount} Antworten und {displayCount} Anzeigen wurden gelöscht.",
|
||||
"survey_results": "{surveyName}-Ergebnisse",
|
||||
"this_month": "Dieser Monat",
|
||||
"this_quarter": "Dieses Quartal",
|
||||
"this_year": "Dieses Jahr",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Färbt den gefüllten Teil des Balkens.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rundet die Eingabeecken ab.",
|
||||
"advanced_styling_field_input_font_size_description": "Skaliert den eingegebenen Text in Eingabefeldern.",
|
||||
"advanced_styling_field_input_height_description": "Legt die Mindesthöhe des Eingabefelds fest.",
|
||||
"advanced_styling_field_input_height_description": "Steuert die Mindesthöhe der Eingabe.",
|
||||
"advanced_styling_field_input_padding_x_description": "Fügt links und rechts Abstand hinzu.",
|
||||
"advanced_styling_field_input_padding_y_description": "Fügt oben und unten Abstand hinzu.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Blendet den Platzhaltertext aus.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Färbt den eingegebenen Text in Eingabefeldern.",
|
||||
"advanced_styling_field_option_bg": "Hintergrund",
|
||||
"advanced_styling_field_option_bg_description": "Füllt die Optionselemente.",
|
||||
"advanced_styling_field_option_border": "Rahmenfarbe",
|
||||
"advanced_styling_field_option_border_description": "Umrandet Radio- und Checkbox-Optionen.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rundet die Ecken der Optionen ab.",
|
||||
"advanced_styling_field_option_font_size_description": "Skaliert den Text der Optionsbeschriftung.",
|
||||
"advanced_styling_field_option_label": "Label-Farbe",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Wie kann das Unternehmen seine Vision und strategische Ausrichtung verbessern?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Tippe deine Antwort hier...",
|
||||
"back": "Zurück",
|
||||
"block_1": "Block 1",
|
||||
"block_10": "Block 10",
|
||||
"block_2": "Block 2",
|
||||
"block_3": "Block 3",
|
||||
"block_4": "Block 4",
|
||||
"block_5": "Block 5",
|
||||
"block_6": "Block 6",
|
||||
"block_7": "Block 7",
|
||||
"block_8": "Block 8",
|
||||
"block_9": "Block 9",
|
||||
"book_interview": "Interview buchen",
|
||||
"build_product_roadmap_description": "Finde die EINE Sache heraus, die deine Nutzer am meisten wollen, und baue sie.",
|
||||
"build_product_roadmap_name": "Produkt Roadmap erstellen",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ugh, sorry! Können wir irgendwas tun, um deine Erfahrung zu verbessern?",
|
||||
"csat_survey_question_3_placeholder": "Tippe deine Antwort hier...",
|
||||
"cta_description": "Information anzeigen und Benutzer auffordern, eine bestimmte Aktion auszuführen",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Erstelle eine Umfrage ohne Vorlage.",
|
||||
"custom_survey_name": "Eigene Umfrage erstellen",
|
||||
"custom_survey_question_1_headline": "Was möchtest Du wissen?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nein, danke!",
|
||||
"preview_survey_question_2_headline": "Möchtest Du auf dem Laufenden bleiben?",
|
||||
"preview_survey_question_2_subheader": "Dies ist eine Beispielbeschreibung.",
|
||||
"preview_survey_question_open_text_headline": "Möchtest Du noch etwas teilen?",
|
||||
"preview_survey_question_open_text_placeholder": "Tippe deine Antwort hier...",
|
||||
"preview_survey_question_open_text_subheader": "Dein Feedback hilft uns, besser zu werden.",
|
||||
"preview_survey_welcome_card_headline": "Willkommen!",
|
||||
"prioritize_features_description": "Identifiziere die Funktionen, die deine Nutzer am meisten und am wenigsten brauchen.",
|
||||
"prioritize_features_name": "Funktionen priorisieren",
|
||||
|
||||
+40
-14
@@ -175,9 +175,12 @@
|
||||
"copy": "Copy",
|
||||
"copy_code": "Copy code",
|
||||
"copy_link": "Copy Link",
|
||||
"count_attributes": "{value, plural, one {{value} attribute} other {{value} attributes}}",
|
||||
"count_contacts": "{value, plural, one {{value} contact} other {{value} contacts}}",
|
||||
"count_responses": "{value, plural, one {{value} response} other {{value} responses}}",
|
||||
"count_attributes": "{count, plural, one {{count} attribute} other {{count} attributes}}",
|
||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
||||
"count_members": "{count, plural, one {{count} member} other {{count} members}}",
|
||||
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
|
||||
"count_responses": "{count, plural, one {{count} response} other {{count} responses}}",
|
||||
"count_selections": "{count, plural, one {{count} selection} other {{count} selections}}",
|
||||
"create_new_organization": "Create new organization",
|
||||
"create_segment": "Create segment",
|
||||
"create_survey": "Create survey",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "days",
|
||||
"default": "Default",
|
||||
"delete": "Delete",
|
||||
"delete_what": "Delete {deleteWhat}",
|
||||
"description": "Description",
|
||||
"dev_env": "Dev Environment",
|
||||
"development": "Development",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Download",
|
||||
"draft": "Draft",
|
||||
"duplicate": "Duplicate",
|
||||
"duplicate_copy": "(copy)",
|
||||
"duplicate_copy_number": "(copy {copyNumber})",
|
||||
"e_commerce": "E-Commerce",
|
||||
"edit": "Edit",
|
||||
"email": "Email",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Failed to load workspaces",
|
||||
"filter": "Filter",
|
||||
"finish": "Finish",
|
||||
"first_name": "First Name",
|
||||
"follow_these": "Follow these",
|
||||
"formbricks_version": "Formbricks Version",
|
||||
"full_name": "Full name",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Hidden field",
|
||||
"hidden_fields": "Hidden fields",
|
||||
"hide_column": "Hide column",
|
||||
"id": "ID",
|
||||
"image": "Image",
|
||||
"images": "Images",
|
||||
"import": "Import",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Key",
|
||||
"label": "Label",
|
||||
"language": "Language",
|
||||
"last_name": "Last Name",
|
||||
"learn_more": "Learn more",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Light overlay",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Look & Feel",
|
||||
"manage": "Manage",
|
||||
"marketing": "Marketing",
|
||||
"member": "Member",
|
||||
"members": "Members",
|
||||
"members_and_teams": "Members & Teams",
|
||||
"membership_not_found": "Membership not found",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Move down",
|
||||
"move_up": "Move up",
|
||||
"multiple_languages": "Multiple languages",
|
||||
"my_product": "my Product",
|
||||
"name": "Name",
|
||||
"new": "New",
|
||||
"new_version_available": "Formbricks {version} is here. Upgrade now!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Select teams",
|
||||
"selected": "Selected",
|
||||
"selected_questions": "Selected questions",
|
||||
"selection": "Selection",
|
||||
"selections": "Selections",
|
||||
"send_test_email": "Send test email",
|
||||
"session_not_found": "Session not found",
|
||||
"settings": "Settings",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Hey {userName}",
|
||||
"email_customization_preview_email_text": "This is an email preview to show you which logo will be rendered in the emails.",
|
||||
"error_deleting_organization_please_try_again": "Error deleting organization. Please try again.",
|
||||
"from_your_organization": "from your organization",
|
||||
"from_your_organization": "{memberName} from your organization",
|
||||
"invitation_sent_once_more": "Invitation sent once more.",
|
||||
"invite_deleted_successfully": "Invite deleted successfully",
|
||||
"invite_expires_on": "Invite expires on {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Add a placeholder to show if there is no value to recall.",
|
||||
"add_hidden_field_id": "Add hidden field ID",
|
||||
"add_highlight_border": "Add highlight border",
|
||||
"add_highlight_border_description": "Only applies to in-product surveys.",
|
||||
"add_logic": "Add logic",
|
||||
"add_none_of_the_above": "Add “None of the Above”",
|
||||
"add_option": "Add option",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Adjust “Survey Closed” message",
|
||||
"adjust_survey_closed_message_description": "Change the message visitors see when the survey is closed.",
|
||||
"adjust_the_theme_in_the": "Adjust the theme in the",
|
||||
"all_are_true": "all are true",
|
||||
"all_other_answers_will_continue_to": "All other answers will continue to",
|
||||
"allow_multi_select": "Allow multi-select",
|
||||
"allow_multiple_files": "Allow multiple files",
|
||||
"allow_users_to_select_more_than_one_image": "Allow users to select more than one image",
|
||||
"and_launch_surveys_in_your_website_or_app": "and launch surveys in your website or app.",
|
||||
"animation": "Animation",
|
||||
"any_is_true": "any is true",
|
||||
"app_survey_description": "Embed a survey in your web app or website to collect responses.",
|
||||
"assign": "Assign =",
|
||||
"audience": "Audience",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Follow-up updated and will be saved once you save the survey.",
|
||||
"follow_ups_new": "New follow-up",
|
||||
"follow_ups_upgrade_button_text": "Upgrade to enable follow-ups",
|
||||
"form_styling": "Form styling",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDK is not connected",
|
||||
"four_points": "4 points",
|
||||
"heading": "Heading",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Option {choiceIndex}",
|
||||
"option_used_in_logic_error": "This option is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"optional": "Optional",
|
||||
"options": "Options",
|
||||
"options": "Options*",
|
||||
"options_used_in_logic_bulk_error": "The following options are used in logic: {questionIndexes}. Please remove them from logic first.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Set custom Cooldown Period",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Question deleted.",
|
||||
"question_duplicated": "Question duplicated.",
|
||||
"question_id_updated": "Question ID updated",
|
||||
"question_number": "Question {number}",
|
||||
"question_used_in_logic_warning_text": "Elements from this block are used in a logic rule, are you sure you want to delete it?",
|
||||
"question_used_in_logic_warning_title": "Logic Inconsistency",
|
||||
"question_used_in_quota": "This question is being used in “{quotaName}” quota",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Response limits, redirections and more.",
|
||||
"response_options": "Response Options",
|
||||
"roundness": "Roundness",
|
||||
"roundness_description": "Controls how rounded the card corners are.",
|
||||
"roundness_description": "Controls how rounded corners are.",
|
||||
"row_used_in_logic_error": "This row is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"rows": "Rows",
|
||||
"save_and_close": "Save & Close",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "This free & open-source survey has been closed",
|
||||
"survey_display_settings": "Survey Display Settings",
|
||||
"survey_placement": "Survey Placement",
|
||||
"survey_styling": "Survey styling",
|
||||
"survey_trigger": "Survey Trigger",
|
||||
"switch_multi_language_on_to_get_started": "Switch multi-language on to get started 👉",
|
||||
"target_block_not_found": "Target block not found",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Cooldown Period (across surveys)",
|
||||
"waiting_time_across_surveys_description": "To prevent survey fatigue, choose how this survey interacts with the workspace-wide Cooldown Period.",
|
||||
"welcome_message": "Welcome message",
|
||||
"when": "When",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Without a filter, all of your users can be surveyed.",
|
||||
"you_have_not_created_a_segment_yet": "You have not created a segment yet",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "You need to have two or more languages set up in your workspace to work with translations.",
|
||||
"your_description_here_recall_information_with": "Your description here. Recall information with @",
|
||||
"your_question_here_recall_information_with": "Your question here. Recall information with @",
|
||||
"your_web_app": "Your web app",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Starts",
|
||||
"starts_tooltip": "Number of times the survey has been started.",
|
||||
"survey_reset_successfully": "Survey reset successfully. {responseCount} responses and {displayCount} displays were deleted.",
|
||||
"survey_results": "{surveyName} Results",
|
||||
"this_month": "This month",
|
||||
"this_quarter": "This quarter",
|
||||
"this_year": "This year",
|
||||
@@ -2157,7 +2169,7 @@
|
||||
"advanced_styling_field_description_size": "Description Font Size",
|
||||
"advanced_styling_field_description_size_description": "Scales the description text.",
|
||||
"advanced_styling_field_description_weight": "Description Font Weight",
|
||||
"advanced_styling_field_description_weight_description": "Makes description text lighter or bolder.",
|
||||
"advanced_styling_field_description_weight_description": "Makes descr. text lighter or bolder.",
|
||||
"advanced_styling_field_font_size": "Font Size",
|
||||
"advanced_styling_field_font_weight": "Font Weight",
|
||||
"advanced_styling_field_headline_color": "Headline Color",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Colors the filled portion of the bar.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rounds the input corners.",
|
||||
"advanced_styling_field_input_font_size_description": "Scales the typed text in inputs.",
|
||||
"advanced_styling_field_input_height_description": "Controls the minimum height of the input field.",
|
||||
"advanced_styling_field_input_height_description": "Controls the min. height of the input.",
|
||||
"advanced_styling_field_input_padding_x_description": "Adds space on the left and right.",
|
||||
"advanced_styling_field_input_padding_y_description": "Adds space on the top and bottom.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Fades the placeholder hint text.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Colors the typed text in inputs.",
|
||||
"advanced_styling_field_option_bg": "Background",
|
||||
"advanced_styling_field_option_bg_description": "Fills the option items.",
|
||||
"advanced_styling_field_option_border": "Border Color",
|
||||
"advanced_styling_field_option_border_description": "Outlines radio and checkbox options.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rounds the option corners.",
|
||||
"advanced_styling_field_option_font_size_description": "Scales the option label text.",
|
||||
"advanced_styling_field_option_label": "Label Color",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "How can the company improve its vision and strategy alignment?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Type your answer here…",
|
||||
"back": "Back",
|
||||
"block_1": "Block 1",
|
||||
"block_10": "Block 10",
|
||||
"block_2": "Block 2",
|
||||
"block_3": "Block 3",
|
||||
"block_4": "Block 4",
|
||||
"block_5": "Block 5",
|
||||
"block_6": "Block 6",
|
||||
"block_7": "Block 7",
|
||||
"block_8": "Block 8",
|
||||
"block_9": "Block 9",
|
||||
"book_interview": "Book interview",
|
||||
"build_product_roadmap_description": "Identify the ONE thing your users want the most and build it.",
|
||||
"build_product_roadmap_name": "Build Product Roadmap",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ugh, sorry! Is there anything we can do to improve your experience?",
|
||||
"csat_survey_question_3_placeholder": "Type your answer here…",
|
||||
"cta_description": "Display information and prompt users to take a specific action",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Create a survey without template.",
|
||||
"custom_survey_name": "Start from scratch",
|
||||
"custom_survey_question_1_headline": "What would you like to know?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "No, thank you!",
|
||||
"preview_survey_question_2_headline": "Want to stay in the loop?",
|
||||
"preview_survey_question_2_subheader": "This is an example description.",
|
||||
"preview_survey_question_open_text_headline": "Anything else you'd like to share?",
|
||||
"preview_survey_question_open_text_placeholder": "Type your answer here…",
|
||||
"preview_survey_question_open_text_subheader": "Your feedback helps us improve.",
|
||||
"preview_survey_welcome_card_headline": "Welcome!",
|
||||
"prioritize_features_description": "Identify features your users need most and least.",
|
||||
"prioritize_features_name": "Prioritize Features",
|
||||
|
||||
+40
-14
@@ -175,9 +175,12 @@
|
||||
"copy": "Copiar",
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar enlace",
|
||||
"count_attributes": "{value, plural, one {{value} atributo} other {{value} atributos}}",
|
||||
"count_contacts": "{value, plural, one {{value} contacto} other {{value} contactos}}",
|
||||
"count_responses": "{value, plural, one {{value} respuesta} other {{value} respuestas}}",
|
||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
||||
"count_members": "{count, plural, one {{count} miembro} other {{count} miembros}}",
|
||||
"count_questions": "{count, plural, one {{count} pregunta} other {{count} preguntas}}",
|
||||
"count_responses": "{count, plural, one {{count} respuesta} other {{count} respuestas}}",
|
||||
"count_selections": "{count, plural, one {{count} selección} other {{count} selecciones}}",
|
||||
"create_new_organization": "Crear organización nueva",
|
||||
"create_segment": "Crear segmento",
|
||||
"create_survey": "Crear encuesta",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "días",
|
||||
"default": "Predeterminado",
|
||||
"delete": "Eliminar",
|
||||
"delete_what": "Eliminar {deleteWhat}",
|
||||
"description": "Descripción",
|
||||
"dev_env": "Entorno de desarrollo",
|
||||
"development": "Desarrollo",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Descargar",
|
||||
"draft": "Borrador",
|
||||
"duplicate": "Duplicar",
|
||||
"duplicate_copy": "(copia)",
|
||||
"duplicate_copy_number": "(copia {copyNumber})",
|
||||
"e_commerce": "Comercio electrónico",
|
||||
"edit": "Editar",
|
||||
"email": "Email",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Error al cargar los proyectos",
|
||||
"filter": "Filtro",
|
||||
"finish": "Finalizar",
|
||||
"first_name": "Nombre",
|
||||
"follow_these": "Sigue estos",
|
||||
"formbricks_version": "Versión de Formbricks",
|
||||
"full_name": "Nombre completo",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Campo oculto",
|
||||
"hidden_fields": "Campos ocultos",
|
||||
"hide_column": "Ocultar columna",
|
||||
"id": "ID",
|
||||
"image": "Imagen",
|
||||
"images": "Imágenes",
|
||||
"import": "Importar",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Clave",
|
||||
"label": "Etiqueta",
|
||||
"language": "Idioma",
|
||||
"last_name": "Apellido",
|
||||
"learn_more": "Saber más",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Superposición clara",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Apariencia",
|
||||
"manage": "Gestionar",
|
||||
"marketing": "Marketing",
|
||||
"member": "Miembro",
|
||||
"members": "Miembros",
|
||||
"members_and_teams": "Miembros y equipos",
|
||||
"membership_not_found": "Membresía no encontrada",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Mover hacia abajo",
|
||||
"move_up": "Mover hacia arriba",
|
||||
"multiple_languages": "Múltiples idiomas",
|
||||
"my_product": "mi producto",
|
||||
"name": "Nombre",
|
||||
"new": "Nuevo",
|
||||
"new_version_available": "Formbricks {version} está aquí. ¡Actualiza ahora!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Seleccionar equipos",
|
||||
"selected": "Seleccionado",
|
||||
"selected_questions": "Preguntas seleccionadas",
|
||||
"selection": "Selección",
|
||||
"selections": "Selecciones",
|
||||
"send_test_email": "Enviar correo electrónico de prueba",
|
||||
"session_not_found": "Sesión no encontrada",
|
||||
"settings": "Ajustes",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Hola {userName}",
|
||||
"email_customization_preview_email_text": "Este es un correo electrónico de vista previa para mostrarte qué logotipo se mostrará en los correos electrónicos.",
|
||||
"error_deleting_organization_please_try_again": "Error al eliminar la organización. Por favor, inténtalo de nuevo.",
|
||||
"from_your_organization": "de tu organización",
|
||||
"from_your_organization": "{memberName} de tu organización",
|
||||
"invitation_sent_once_more": "Invitación enviada una vez más.",
|
||||
"invite_deleted_successfully": "Invitación eliminada correctamente",
|
||||
"invite_expires_on": "La invitación expira el {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Añadir un marcador de posición para mostrar si no hay valor que recuperar.",
|
||||
"add_hidden_field_id": "Añadir ID de campo oculto",
|
||||
"add_highlight_border": "Añadir borde destacado",
|
||||
"add_highlight_border_description": "Solo se aplica a encuestas dentro del producto.",
|
||||
"add_logic": "Añadir lógica",
|
||||
"add_none_of_the_above": "Añadir \"Ninguna de las anteriores\"",
|
||||
"add_option": "Añadir opción",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensaje 'Encuesta cerrada'",
|
||||
"adjust_survey_closed_message_description": "Cambiar el mensaje que ven los visitantes cuando la encuesta está cerrada.",
|
||||
"adjust_the_theme_in_the": "Ajustar el tema en el",
|
||||
"all_are_true": "todas son verdaderas",
|
||||
"all_other_answers_will_continue_to": "Todas las demás respuestas continuarán",
|
||||
"allow_multi_select": "Permitir selección múltiple",
|
||||
"allow_multiple_files": "Permitir múltiples archivos",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir a los usuarios seleccionar más de una imagen",
|
||||
"and_launch_surveys_in_your_website_or_app": "y lanzar encuestas en tu sitio web o aplicación.",
|
||||
"animation": "Animación",
|
||||
"any_is_true": "alguna es verdadera",
|
||||
"app_survey_description": "Integra una encuesta en tu aplicación web o sitio web para recopilar respuestas.",
|
||||
"assign": "Asignar =",
|
||||
"audience": "Audiencia",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Seguimiento actualizado y se guardará cuando guardes la encuesta.",
|
||||
"follow_ups_new": "Nuevo seguimiento",
|
||||
"follow_ups_upgrade_button_text": "Actualiza para habilitar seguimientos",
|
||||
"form_styling": "Estilo del formulario",
|
||||
"formbricks_sdk_is_not_connected": "El SDK de Formbricks no está conectado",
|
||||
"four_points": "4 puntos",
|
||||
"heading": "Encabezado",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Opción {choiceIndex}",
|
||||
"option_used_in_logic_error": "Esta opción se utiliza en la lógica de la pregunta {questionIndex}. Por favor, elimínala de la lógica primero.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opciones",
|
||||
"options": "Opciones*",
|
||||
"options_used_in_logic_bulk_error": "Las siguientes opciones se utilizan en la lógica: {questionIndexes}. Por favor, elimínalas de la lógica primero.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Anular el tema con estilos individuales para esta encuesta.",
|
||||
"overwrite_global_waiting_time": "Establecer periodo de espera personalizado",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Pregunta eliminada.",
|
||||
"question_duplicated": "Pregunta duplicada.",
|
||||
"question_id_updated": "ID de pregunta actualizado",
|
||||
"question_number": "Pregunta {number}",
|
||||
"question_used_in_logic_warning_text": "Los elementos de este bloque se usan en una regla de lógica, ¿estás seguro de que quieres eliminarlo?",
|
||||
"question_used_in_logic_warning_title": "Inconsistencia de lógica",
|
||||
"question_used_in_quota": "Esta pregunta se está utilizando en la cuota “{quotaName}”",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Límites de respuestas, redirecciones y más.",
|
||||
"response_options": "Opciones de respuesta",
|
||||
"roundness": "Redondez",
|
||||
"roundness_description": "Controla qué tan redondeadas están las esquinas de la tarjeta.",
|
||||
"roundness_description": "Controla qué tan redondeadas están las esquinas.",
|
||||
"row_used_in_logic_error": "Esta fila se utiliza en la lógica de la pregunta {questionIndex}. Por favor, elimínala de la lógica primero.",
|
||||
"rows": "Filas",
|
||||
"save_and_close": "Guardar y cerrar",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Esta encuesta gratuita y de código abierto ha sido cerrada",
|
||||
"survey_display_settings": "Ajustes de visualización de la encuesta",
|
||||
"survey_placement": "Ubicación de la encuesta",
|
||||
"survey_styling": "Estilo del formulario",
|
||||
"survey_trigger": "Activador de la encuesta",
|
||||
"switch_multi_language_on_to_get_started": "Activa el modo multiidioma para comenzar 👉",
|
||||
"target_block_not_found": "Bloque objetivo no encontrado",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Periodo de espera (entre encuestas)",
|
||||
"waiting_time_across_surveys_description": "Para evitar la fatiga de encuestas, elige cómo interactúa esta encuesta con el periodo de espera general del espacio de trabajo.",
|
||||
"welcome_message": "Mensaje de bienvenida",
|
||||
"when": "Cuando",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sin un filtro, todos tus usuarios pueden ser encuestados.",
|
||||
"you_have_not_created_a_segment_yet": "Aún no has creado un segmento",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Necesitas tener dos o más idiomas configurados en tu proyecto para trabajar con traducciones.",
|
||||
"your_description_here_recall_information_with": "Tu descripción aquí. Recupera información con @",
|
||||
"your_question_here_recall_information_with": "Tu pregunta aquí. Recupera información con @",
|
||||
"your_web_app": "Tu aplicación web",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Inicios",
|
||||
"starts_tooltip": "Número de veces que se ha iniciado la encuesta.",
|
||||
"survey_reset_successfully": "¡Encuesta restablecida correctamente! Se eliminaron {responseCount} respuestas y {displayCount} visualizaciones.",
|
||||
"survey_results": "Resultados de {surveyName}",
|
||||
"this_month": "Este mes",
|
||||
"this_quarter": "Este trimestre",
|
||||
"this_year": "Este año",
|
||||
@@ -2157,7 +2169,7 @@
|
||||
"advanced_styling_field_description_size": "Tamaño de fuente de la descripción",
|
||||
"advanced_styling_field_description_size_description": "Escala el texto de la descripción.",
|
||||
"advanced_styling_field_description_weight": "Grosor de fuente de la descripción",
|
||||
"advanced_styling_field_description_weight_description": "Hace el texto de la descripción más ligero o más grueso.",
|
||||
"advanced_styling_field_description_weight_description": "Hace el texto de descripción más ligero o más grueso.",
|
||||
"advanced_styling_field_font_size": "Tamaño de fuente",
|
||||
"advanced_styling_field_font_weight": "Grosor de fuente",
|
||||
"advanced_styling_field_headline_color": "Color del titular",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Colorea la porción rellena de la barra.",
|
||||
"advanced_styling_field_input_border_radius_description": "Redondea las esquinas del campo.",
|
||||
"advanced_styling_field_input_font_size_description": "Escala el texto escrito en los campos.",
|
||||
"advanced_styling_field_input_height_description": "Controla la altura mínima del campo de entrada.",
|
||||
"advanced_styling_field_input_height_description": "Controla la altura mínima de la entrada.",
|
||||
"advanced_styling_field_input_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
|
||||
"advanced_styling_field_input_padding_y_description": "Añade espacio en la parte superior e inferior.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Atenúa el texto de sugerencia del marcador de posición.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Colorea el texto escrito en los campos de entrada.",
|
||||
"advanced_styling_field_option_bg": "Fondo",
|
||||
"advanced_styling_field_option_bg_description": "Rellena los elementos de opción.",
|
||||
"advanced_styling_field_option_border": "Color del borde",
|
||||
"advanced_styling_field_option_border_description": "Delimita las opciones de radio y casillas de verificación.",
|
||||
"advanced_styling_field_option_border_radius_description": "Redondea las esquinas de las opciones.",
|
||||
"advanced_styling_field_option_font_size_description": "Escala el texto de la etiqueta de opción.",
|
||||
"advanced_styling_field_option_label": "Color de la etiqueta",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "¿Cómo puede mejorar la empresa su alineación de visión y estrategia?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Escribe tu respuesta aquí...",
|
||||
"back": "Atrás",
|
||||
"block_1": "Bloque 1",
|
||||
"block_10": "Bloque 10",
|
||||
"block_2": "Bloque 2",
|
||||
"block_3": "Bloque 3",
|
||||
"block_4": "Bloque 4",
|
||||
"block_5": "Bloque 5",
|
||||
"block_6": "Bloque 6",
|
||||
"block_7": "Bloque 7",
|
||||
"block_8": "Bloque 8",
|
||||
"block_9": "Bloque 9",
|
||||
"book_interview": "Reservar entrevista",
|
||||
"build_product_roadmap_description": "Identifica lo ÚNICO que tus usuarios desean más y constrúyelo.",
|
||||
"build_product_roadmap_name": "Crear hoja de ruta del producto",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Vaya, ¡lo sentimos! ¿Hay algo que podamos hacer para mejorar tu experiencia?",
|
||||
"csat_survey_question_3_placeholder": "Escribe tu respuesta aquí...",
|
||||
"cta_description": "Muestra información y anima a los usuarios a realizar una acción específica",
|
||||
"custom_survey_block_1_name": "Bloque 1",
|
||||
"custom_survey_description": "Crea una encuesta sin plantilla.",
|
||||
"custom_survey_name": "Empezar desde cero",
|
||||
"custom_survey_question_1_headline": "¿Qué te gustaría saber?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "¡No, gracias!",
|
||||
"preview_survey_question_2_headline": "¿Quieres estar al tanto?",
|
||||
"preview_survey_question_2_subheader": "Esta es una descripción de ejemplo.",
|
||||
"preview_survey_question_open_text_headline": "¿Hay algo más que te gustaría compartir?",
|
||||
"preview_survey_question_open_text_placeholder": "Escribe tu respuesta aquí...",
|
||||
"preview_survey_question_open_text_subheader": "Tus comentarios nos ayudan a mejorar.",
|
||||
"preview_survey_welcome_card_headline": "¡Bienvenido!",
|
||||
"prioritize_features_description": "Identifica las funciones que tus usuarios necesitan más y menos.",
|
||||
"prioritize_features_name": "Priorizar funciones",
|
||||
|
||||
+39
-14
@@ -112,7 +112,6 @@
|
||||
"link_expired_description": "Le lien que vous avez utilisé n'est plus valide."
|
||||
},
|
||||
"common": {
|
||||
"Filter": "Filtrer",
|
||||
"accepted": "Accepté",
|
||||
"account": "Compte",
|
||||
"account_settings": "Paramètres du compte",
|
||||
@@ -176,9 +175,12 @@
|
||||
"copy": "Copier",
|
||||
"copy_code": "Copier le code",
|
||||
"copy_link": "Copier le lien",
|
||||
"count_attributes": "{value, plural, one {{value} attribut} other {{value} attributs}}",
|
||||
"count_contacts": "{value, plural, one {# contact} other {# contacts} }",
|
||||
"count_responses": "{value, plural, other {# réponses}}",
|
||||
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attributs}}",
|
||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
||||
"count_members": "{count, plural, one {{count} membre} other {{count} membres}}",
|
||||
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
|
||||
"count_responses": "{count, plural, other {# réponses}}",
|
||||
"count_selections": "{count, plural, one {{count} sélection} other {{count} sélections}}",
|
||||
"create_new_organization": "Créer une nouvelle organisation",
|
||||
"create_segment": "Créer un segment",
|
||||
"create_survey": "Créer un sondage",
|
||||
@@ -192,6 +194,7 @@
|
||||
"days": "jours",
|
||||
"default": "Par défaut",
|
||||
"delete": "Supprimer",
|
||||
"delete_what": "Supprimer {deleteWhat}",
|
||||
"description": "Description",
|
||||
"dev_env": "Environnement de développement",
|
||||
"development": "Développement",
|
||||
@@ -207,6 +210,8 @@
|
||||
"download": "Télécharger",
|
||||
"draft": "Brouillon",
|
||||
"duplicate": "Dupliquer",
|
||||
"duplicate_copy": "(copie)",
|
||||
"duplicate_copy_number": "(copie {copyNumber})",
|
||||
"e_commerce": "E-commerce",
|
||||
"edit": "Modifier",
|
||||
"email": "Email",
|
||||
@@ -228,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Échec du chargement des projets",
|
||||
"filter": "Filtre",
|
||||
"finish": "Terminer",
|
||||
"first_name": "Prénom",
|
||||
"follow_these": "Suivez ceci",
|
||||
"formbricks_version": "Version de Formbricks",
|
||||
"full_name": "Nom complet",
|
||||
@@ -240,6 +246,7 @@
|
||||
"hidden_field": "Champ caché",
|
||||
"hidden_fields": "Champs cachés",
|
||||
"hide_column": "Cacher la colonne",
|
||||
"id": "ID",
|
||||
"image": "Image",
|
||||
"images": "Images",
|
||||
"import": "Importer",
|
||||
@@ -257,6 +264,7 @@
|
||||
"key": "Clé",
|
||||
"label": "Étiquette",
|
||||
"language": "Langue",
|
||||
"last_name": "Nom de famille",
|
||||
"learn_more": "En savoir plus",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Claire",
|
||||
@@ -271,7 +279,6 @@
|
||||
"look_and_feel": "Apparence",
|
||||
"manage": "Gérer",
|
||||
"marketing": "Marketing",
|
||||
"member": "Membre",
|
||||
"members": "Membres",
|
||||
"members_and_teams": "Membres & Équipes",
|
||||
"membership_not_found": "Abonnement non trouvé",
|
||||
@@ -283,6 +290,7 @@
|
||||
"move_down": "Déplacer vers le bas",
|
||||
"move_up": "Déplacer vers le haut",
|
||||
"multiple_languages": "Plusieurs langues",
|
||||
"my_product": "mon produit",
|
||||
"name": "Nom",
|
||||
"new": "Nouveau",
|
||||
"new_version_available": "Formbricks {version} est là. Mettez à jour maintenant !",
|
||||
@@ -378,8 +386,6 @@
|
||||
"select_teams": "Sélectionner les équipes",
|
||||
"selected": "Sélectionné",
|
||||
"selected_questions": "Questions sélectionnées",
|
||||
"selection": "Sélection",
|
||||
"selections": "Sélections",
|
||||
"send_test_email": "Envoyer un e-mail de test",
|
||||
"session_not_found": "Session non trouvée",
|
||||
"settings": "Paramètres",
|
||||
@@ -1079,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Salut {userName}",
|
||||
"email_customization_preview_email_text": "Cette est une prévisualisation d'e-mail pour vous montrer quel logo sera rendu dans les e-mails.",
|
||||
"error_deleting_organization_please_try_again": "Erreur lors de la suppression de l'organisation. Veuillez réessayer.",
|
||||
"from_your_organization": "de votre organisation",
|
||||
"from_your_organization": "{memberName} de votre organisation",
|
||||
"invitation_sent_once_more": "Invitation envoyée une fois de plus.",
|
||||
"invite_deleted_successfully": "Invitation supprimée avec succès",
|
||||
"invite_expires_on": "L'invitation expire le {date}",
|
||||
@@ -1244,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Ajouter un espace réservé à afficher s'il n'y a pas de valeur à rappeler.",
|
||||
"add_hidden_field_id": "Ajouter un champ caché ID",
|
||||
"add_highlight_border": "Ajouter une bordure de surlignage",
|
||||
"add_highlight_border_description": "S'applique uniquement aux sondages intégrés au produit.",
|
||||
"add_logic": "Ajouter de la logique",
|
||||
"add_none_of_the_above": "Ajouter \"Aucun des éléments ci-dessus\"",
|
||||
"add_option": "Ajouter une option",
|
||||
@@ -1260,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajuster le message \"Sondage fermé\"",
|
||||
"adjust_survey_closed_message_description": "Modifiez le message que les visiteurs voient lorsque l'enquête est fermée.",
|
||||
"adjust_the_theme_in_the": "Ajustez le thème dans le",
|
||||
"all_are_true": "toutes sont vraies",
|
||||
"all_other_answers_will_continue_to": "Toutes les autres réponses continueront à",
|
||||
"allow_multi_select": "Autoriser la sélection multiple",
|
||||
"allow_multiple_files": "Autoriser plusieurs fichiers",
|
||||
"allow_users_to_select_more_than_one_image": "Permettre aux utilisateurs de sélectionner plusieurs images",
|
||||
"and_launch_surveys_in_your_website_or_app": "et lancez des enquêtes sur votre site web ou votre application.",
|
||||
"animation": "Animation",
|
||||
"any_is_true": "au moins une est vraie",
|
||||
"app_survey_description": "Intégrez une enquête dans votre application web ou votre site web pour collecter des réponses.",
|
||||
"assign": "Attribuer =",
|
||||
"audience": "Public",
|
||||
@@ -1442,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "\"Suivi mis à jour et sera enregistré une fois que vous sauvegarderez le sondage.\"",
|
||||
"follow_ups_new": "Nouveau suivi",
|
||||
"follow_ups_upgrade_button_text": "Passez à la version supérieure pour activer les relances",
|
||||
"form_styling": "Style de formulaire",
|
||||
"formbricks_sdk_is_not_connected": "Le SDK Formbricks n'est pas connecté",
|
||||
"four_points": "4 points",
|
||||
"heading": "En-tête",
|
||||
@@ -1532,7 +1540,7 @@
|
||||
"option_idx": "Option {choiceIndex}",
|
||||
"option_used_in_logic_error": "Cette option est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
||||
"optional": "Optionnel",
|
||||
"options": "Options",
|
||||
"options": "Options*",
|
||||
"options_used_in_logic_bulk_error": "Les options suivantes sont utilisées dans la logique : {questionIndexes}. Veuillez d'abord les supprimer de la logique.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Définir une période de refroidissement personnalisée",
|
||||
@@ -1557,6 +1565,7 @@
|
||||
"question_deleted": "Question supprimée.",
|
||||
"question_duplicated": "Question dupliquée.",
|
||||
"question_id_updated": "ID de la question mis à jour",
|
||||
"question_number": "Question {number}",
|
||||
"question_used_in_logic_warning_text": "Des éléments de ce bloc sont utilisés dans une règle logique, êtes-vous sûr de vouloir le supprimer ?",
|
||||
"question_used_in_logic_warning_title": "Incohérence de logique",
|
||||
"question_used_in_quota": "Cette question est utilisée dans le quota “{quotaName}”",
|
||||
@@ -1615,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Limites de réponse, redirections et plus.",
|
||||
"response_options": "Options de réponse",
|
||||
"roundness": "Rondeur",
|
||||
"roundness_description": "Contrôle l'arrondi des coins de la carte.",
|
||||
"roundness_description": "Contrôle l'arrondi des coins.",
|
||||
"row_used_in_logic_error": "Cette ligne est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
||||
"rows": "Lignes",
|
||||
"save_and_close": "Enregistrer et fermer",
|
||||
@@ -1661,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Cette enquête gratuite et open-source a été fermée",
|
||||
"survey_display_settings": "Paramètres d'affichage de l'enquête",
|
||||
"survey_placement": "Placement de l'enquête",
|
||||
"survey_styling": "Style de formulaire",
|
||||
"survey_trigger": "Déclencheur d'enquête",
|
||||
"switch_multi_language_on_to_get_started": "Activez le mode multilingue pour commencer 👉",
|
||||
"target_block_not_found": "Bloc cible non trouvé",
|
||||
@@ -1749,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Période de refroidissement (entre les sondages)",
|
||||
"waiting_time_across_surveys_description": "Pour éviter la fatigue liée aux sondages, choisissez comment ce sondage interagit avec la période de refroidissement globale de l'espace de travail.",
|
||||
"welcome_message": "Message de bienvenue",
|
||||
"when": "Quand",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sans filtre, tous vos utilisateurs peuvent être sondés.",
|
||||
"you_have_not_created_a_segment_yet": "Tu n'as pas encore créé de segment.",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Vous devez avoir deux langues ou plus configurées dans votre espace de travail pour travailler avec les traductions.",
|
||||
"your_description_here_recall_information_with": "Votre description ici. Rappelez-vous des informations avec @",
|
||||
"your_question_here_recall_information_with": "Votre question ici. Rappelez-vous des informations avec @",
|
||||
"your_web_app": "Votre application web",
|
||||
@@ -2025,6 +2035,7 @@
|
||||
"starts": "Commence",
|
||||
"starts_tooltip": "Nombre de fois que l'enquête a été commencée.",
|
||||
"survey_reset_successfully": "Réinitialisation du sondage réussie ! {responseCount} réponses et {displayCount} affichages ont été supprimés.",
|
||||
"survey_results": "Résultats de {surveyName}",
|
||||
"this_month": "Ce mois-ci",
|
||||
"this_quarter": "Ce trimestre",
|
||||
"this_year": "Cette année",
|
||||
@@ -2172,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Colore la partie remplie de la barre.",
|
||||
"advanced_styling_field_input_border_radius_description": "Arrondit les coins du champ de saisie.",
|
||||
"advanced_styling_field_input_font_size_description": "Ajuste la taille du texte saisi dans les champs.",
|
||||
"advanced_styling_field_input_height_description": "Contrôle la hauteur minimale du champ de saisie.",
|
||||
"advanced_styling_field_input_height_description": "Contrôle la hauteur min. du champ de saisie.",
|
||||
"advanced_styling_field_input_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
|
||||
"advanced_styling_field_input_padding_y_description": "Ajoute de l'espace en haut et en bas.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Atténue le texte d'indication du placeholder.",
|
||||
@@ -2181,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Colore le texte saisi dans les champs.",
|
||||
"advanced_styling_field_option_bg": "Arrière-plan",
|
||||
"advanced_styling_field_option_bg_description": "Remplit les éléments d'option.",
|
||||
"advanced_styling_field_option_border": "Couleur de bordure",
|
||||
"advanced_styling_field_option_border_description": "Contours des options de boutons radio et de cases à cocher.",
|
||||
"advanced_styling_field_option_border_radius_description": "Arrondit les coins des options.",
|
||||
"advanced_styling_field_option_font_size_description": "Ajuste la taille du texte des libellés d'option.",
|
||||
"advanced_styling_field_option_label": "Couleur de l'étiquette",
|
||||
@@ -2390,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Comment l'entreprise peut-elle améliorer l'alignement de sa vision et de sa stratégie ?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Entrez votre réponse ici...",
|
||||
"back": "Retour",
|
||||
"block_1": "Bloc 1",
|
||||
"block_10": "Bloc 10",
|
||||
"block_2": "Bloc 2",
|
||||
"block_3": "Bloc 3",
|
||||
"block_4": "Bloc 4",
|
||||
"block_5": "Bloc 5",
|
||||
"block_6": "Bloc 6",
|
||||
"block_7": "Bloc 7",
|
||||
"block_8": "Bloc 8",
|
||||
"block_9": "Bloc 9",
|
||||
"book_interview": "Réserver un entretien",
|
||||
"build_product_roadmap_description": "Identifiez la chose UNIQUE que vos utilisateurs désirent le plus et construisez-la.",
|
||||
"build_product_roadmap_name": "Élaborer la feuille de route du produit",
|
||||
@@ -2597,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ah, désolé ! Y a-t-il quelque chose que nous puissions faire pour améliorer votre expérience ?",
|
||||
"csat_survey_question_3_placeholder": "Entrez votre réponse ici...",
|
||||
"cta_description": "Afficher des informations et inciter les utilisateurs à effectuer une action spécifique",
|
||||
"custom_survey_block_1_name": "Bloc 1",
|
||||
"custom_survey_description": "Créez une enquête sans utiliser de modèle.",
|
||||
"custom_survey_name": "Tout créer moi-même",
|
||||
"custom_survey_question_1_headline": "Que voudriez-vous savoir ?",
|
||||
@@ -3000,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Non, merci !",
|
||||
"preview_survey_question_2_headline": "Souhaitez-vous être informé ?",
|
||||
"preview_survey_question_2_subheader": "Ceci est un exemple de description.",
|
||||
"preview_survey_question_open_text_headline": "Autre chose que vous aimeriez partager ?",
|
||||
"preview_survey_question_open_text_placeholder": "Entrez votre réponse ici...",
|
||||
"preview_survey_question_open_text_subheader": "Vos commentaires nous aident à nous améliorer.",
|
||||
"preview_survey_welcome_card_headline": "Bienvenue !",
|
||||
"prioritize_features_description": "Identifiez les fonctionnalités dont vos utilisateurs ont le plus et le moins besoin.",
|
||||
"prioritize_features_name": "Prioriser les fonctionnalités",
|
||||
|
||||
+42
-16
@@ -175,9 +175,12 @@
|
||||
"copy": "Másolás",
|
||||
"copy_code": "Kód másolása",
|
||||
"copy_link": "Hivatkozás másolása",
|
||||
"count_attributes": "{value, plural, one {{value} attribútum} other {{value} attribútum}}",
|
||||
"count_contacts": "{value, plural, one {{value} partner} other {{value} partner}}",
|
||||
"count_responses": "{value, plural, one {{value} válasz} other {{value} válasz}}",
|
||||
"count_attributes": "{count, plural, one {{count} attribútum} other {{count} attribútum}}",
|
||||
"count_contacts": "{count, plural, one {{count} partner} other {{count} partner}}",
|
||||
"count_members": "{count, plural, one {{count} tag} other {{count} tag}}",
|
||||
"count_questions": "{count, plural, one {{count} kérdés} other {{count} kérdés}}",
|
||||
"count_responses": "{count, plural, one {{count} válasz} other {{count} válasz}}",
|
||||
"count_selections": "{count, plural, one {{count} kijelölés} other {{count} kijelölés}}",
|
||||
"create_new_organization": "Új szervezet létrehozása",
|
||||
"create_segment": "Szakasz létrehozása",
|
||||
"create_survey": "Kérdőív létrehozása",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "napok",
|
||||
"default": "Alapértelmezett",
|
||||
"delete": "Törlés",
|
||||
"delete_what": "{deleteWhat} törlése",
|
||||
"description": "Leírás",
|
||||
"dev_env": "Fejlesztői környezet",
|
||||
"development": "Fejlesztés",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Letöltés",
|
||||
"draft": "Piszkozat",
|
||||
"duplicate": "Kettőzés",
|
||||
"duplicate_copy": "(másolat)",
|
||||
"duplicate_copy_number": "({copyNumber}. másolat)",
|
||||
"e_commerce": "E-kereskedelem",
|
||||
"edit": "Szerkesztés",
|
||||
"email": "E-mail",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Nem sikerült a munkaterületek betöltése",
|
||||
"filter": "Szűrő",
|
||||
"finish": "Befejezés",
|
||||
"first_name": "Keresztnév",
|
||||
"follow_these": "Ezek követése",
|
||||
"formbricks_version": "Formbricks verziója",
|
||||
"full_name": "Teljes név",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Rejtett mező",
|
||||
"hidden_fields": "Rejtett mezők",
|
||||
"hide_column": "Oszlop elrejtése",
|
||||
"id": "ID",
|
||||
"image": "Kép",
|
||||
"images": "Képek",
|
||||
"import": "Importálás",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Kulcs",
|
||||
"label": "Címke",
|
||||
"language": "Nyelv",
|
||||
"last_name": "Vezetéknév",
|
||||
"learn_more": "Tudjon meg többet",
|
||||
"license_expired": "A licenc lejárt",
|
||||
"light_overlay": "Világos rávetítés",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Megjelenés",
|
||||
"manage": "Kezelés",
|
||||
"marketing": "Marketing",
|
||||
"member": "Tag",
|
||||
"members": "Tagok",
|
||||
"members_and_teams": "Tagok és csapatok",
|
||||
"membership_not_found": "A tagság nem található",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Mozgatás le",
|
||||
"move_up": "Mozgatás fel",
|
||||
"multiple_languages": "Több nyelv",
|
||||
"my_product": "saját termék",
|
||||
"name": "Név",
|
||||
"new": "Új",
|
||||
"new_version_available": "A Formbricks {version} megérkezett. Frissítsen most!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Csapatok kiválasztása",
|
||||
"selected": "Kiválasztva",
|
||||
"selected_questions": "Kiválasztott kérdések",
|
||||
"selection": "Kiválasztás",
|
||||
"selections": "Kiválasztások",
|
||||
"send_test_email": "Teszt e-mail küldése",
|
||||
"session_not_found": "A munkamenet nem található",
|
||||
"settings": "Beállítások",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Helló {userName}",
|
||||
"email_customization_preview_email_text": "Ez egy e-mail előnézet, amely azt mutatja meg, hogy melyik logó fog megjelenni az e-mailekben.",
|
||||
"error_deleting_organization_please_try_again": "Hiba a szervezet törlésekor. Próbálja meg újra.",
|
||||
"from_your_organization": "a szervezetétől",
|
||||
"from_your_organization": "{memberName} a szervezetből",
|
||||
"invitation_sent_once_more": "A meghívó még egyszer elküldve.",
|
||||
"invite_deleted_successfully": "A meghívó sikeresen törölve",
|
||||
"invite_expires_on": "A meghívó lejár ekkor: {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Helykitöltő hozzáadása annak megjelenítéshez, hogy nincs visszahívandó érték.",
|
||||
"add_hidden_field_id": "Rejtett mezőazonosító hozzáadása",
|
||||
"add_highlight_border": "Kiemelési szegély hozzáadása",
|
||||
"add_highlight_border_description": "Csak a terméken belüli felmérésekre vonatkozik.",
|
||||
"add_logic": "Logika hozzáadása",
|
||||
"add_none_of_the_above": "„A fentiek közül egyik sem” hozzáadása",
|
||||
"add_option": "Lehetőség hozzáadása",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "A „Kérdőív lezárva” üzenet módosítása",
|
||||
"adjust_survey_closed_message_description": "Annak az üzenetnek a megváltoztatása, amelyet a látogatók akkor látnak, amikor a kérdőív lezárul.",
|
||||
"adjust_the_theme_in_the": "A téma beállítása ebben:",
|
||||
"all_are_true": "az összes igaz",
|
||||
"all_other_answers_will_continue_to": "Az összes többi válasz továbbra is",
|
||||
"allow_multi_select": "Több választás engedélyezése",
|
||||
"allow_multiple_files": "Több fájl engedélyezése",
|
||||
"allow_users_to_select_more_than_one_image": "Lehetővé tétel a felhasználóknak, hogy egynél több képet válasszanak ki",
|
||||
"and_launch_surveys_in_your_website_or_app": "és kérdőívek indítása a webhelyén vagy az alkalmazásában.",
|
||||
"animation": "Animáció",
|
||||
"any_is_true": "bármelyik igaz",
|
||||
"app_survey_description": "Egy kérdőív beágyazása a webalkalmazásába vagy webhelyére a válaszok gyűjtéséhez.",
|
||||
"assign": "= hozzárendelése",
|
||||
"audience": "Közönség",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "A követés frissítve, és akkor lesz elmentve, ha elmenti a kérdőívet.",
|
||||
"follow_ups_new": "Új követés",
|
||||
"follow_ups_upgrade_button_text": "Magasabb csomagra váltás a követések engedélyezéséhez",
|
||||
"form_styling": "Űrlap stílusának beállítása",
|
||||
"formbricks_sdk_is_not_connected": "A Formbricks SDK nincs csatlakoztatva",
|
||||
"four_points": "4 pont",
|
||||
"heading": "Címsor",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "{choiceIndex}. lehetőség",
|
||||
"option_used_in_logic_error": "Ez a lehetőség használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
||||
"optional": "Választható",
|
||||
"options": "Beállítások",
|
||||
"options": "Beállítások*",
|
||||
"options_used_in_logic_bulk_error": "A következő lehetőségek használatban vannak a logikában: {questionIndexes}. Először távolítsa el azokat a logikából.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "A téma felülírása egyéni stílusokkal ennél a kérdőívnél.",
|
||||
"overwrite_global_waiting_time": "Egyéni várakozási időszak beállítása",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Kérdés törölve.",
|
||||
"question_duplicated": "Kérdés megkettőzve.",
|
||||
"question_id_updated": "Kérdésazonosító frissítve",
|
||||
"question_number": "{number}. kérdés",
|
||||
"question_used_in_logic_warning_text": "Ezen blokkból származó elemek egy logikai szabályban vannak használva, biztosan törölni szeretné?",
|
||||
"question_used_in_logic_warning_title": "Logikai következetlenség",
|
||||
"question_used_in_quota": "Ez a kérdés használatban van a(z) „{quotaName}” kvótában",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Válaszkorlátok, átirányítások és egyebek.",
|
||||
"response_options": "Válasz beállításai",
|
||||
"roundness": "Kerekesség",
|
||||
"roundness_description": "Annak vezérlése, hogy a kártya sarkai mennyire legyenek lekerekítve.",
|
||||
"roundness_description": "Szabályozza a sarkok lekerekítését.",
|
||||
"row_used_in_logic_error": "Ez a sor használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
||||
"rows": "Sorok",
|
||||
"save_and_close": "Mentés és bezárás",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Ez a szabad és nyílt forráskódú kérdőív le lett zárva",
|
||||
"survey_display_settings": "Kérdőív megjelenítésének beállításai",
|
||||
"survey_placement": "Kérdőív elhelyezése",
|
||||
"survey_styling": "Űrlap stílusának beállítása",
|
||||
"survey_trigger": "Kérdőív aktiválója",
|
||||
"switch_multi_language_on_to_get_started": "Kapcsolja be a többnyelvűséget a kezdéshez 👉",
|
||||
"target_block_not_found": "A célblokk nem található",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Várakozási időszak (kérdőívek között)",
|
||||
"waiting_time_across_surveys_description": "A kérdőívekbe való belefáradás megakadályozásához válassza ki, hogy ez a kérdőív hogyan lép kölcsönhatásba a munkaterület-szintű várakozási időszakkal.",
|
||||
"welcome_message": "Üdvözlő üzenet",
|
||||
"when": "Amikor",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Szűrő nélkül az összes felhasználója megkérdezhető.",
|
||||
"you_have_not_created_a_segment_yet": "Még nem hozott létre szakaszt",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Be kell állítania kettő vagy több nyelvet a munkaterületen a fordításokkal való munkához.",
|
||||
"your_description_here_recall_information_with": "Ide jön a leírás. Információk visszahívása a @ karakterrel.",
|
||||
"your_question_here_recall_information_with": "Ide jön a kérdés. Információk visszahívása a @ karakterrel.",
|
||||
"your_web_app": "Saját webalkalmazás",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Elkezdések",
|
||||
"starts_tooltip": "A kérdőív elkezdési alkalmainak száma.",
|
||||
"survey_reset_successfully": "A kérdőív sikeresen visszaállítva. {responseCount} válasz és {displayCount} megjelenítés lett törölve.",
|
||||
"survey_results": "{surveyName} eredményei",
|
||||
"this_month": "Ez a hónap",
|
||||
"this_quarter": "Ez a negyedév",
|
||||
"this_year": "Ez az év",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Kiszínezi a sáv kitöltött részét.",
|
||||
"advanced_styling_field_input_border_radius_description": "Lekerekíti a beviteli mező sarkait.",
|
||||
"advanced_styling_field_input_font_size_description": "Átméretezi a beviteli mezőkbe beírt szöveget.",
|
||||
"advanced_styling_field_input_height_description": "A beviteli mező minimális magasságát szabályozza.",
|
||||
"advanced_styling_field_input_height_description": "Szabályozza a beviteli mező minimális magasságát.",
|
||||
"advanced_styling_field_input_padding_x_description": "Térközt ad hozzá balra és jobbra.",
|
||||
"advanced_styling_field_input_padding_y_description": "Térközt ad hozzá fent és lent.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Elhalványítja a helykitöltő súgószöveget.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Kiszínezi a beviteli mezőkbe beírt szöveget.",
|
||||
"advanced_styling_field_option_bg": "Háttér",
|
||||
"advanced_styling_field_option_bg_description": "Kitölti a választási lehetőség elemeit.",
|
||||
"advanced_styling_field_option_border": "Szegély színe",
|
||||
"advanced_styling_field_option_border_description": "A rádiógomb és jelölőnégyzet opciók körvonalát határozza meg.",
|
||||
"advanced_styling_field_option_border_radius_description": "Lekerekíti a választási lehetőség sarkait.",
|
||||
"advanced_styling_field_option_font_size_description": "Átméretezi a választási lehetőség címkéjének szövegét.",
|
||||
"advanced_styling_field_option_label": "Címke színe",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Hogyan tudná javítani a vállalat a jövőképe és stratégiája összehangolását?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Írja be ide a válaszát…",
|
||||
"back": "Vissza",
|
||||
"block_1": "1. blokk",
|
||||
"block_10": "10. blokk",
|
||||
"block_2": "2. blokk",
|
||||
"block_3": "3. blokk",
|
||||
"block_4": "4. blokk",
|
||||
"block_5": "5. blokk",
|
||||
"block_6": "6. blokk",
|
||||
"block_7": "7. blokk",
|
||||
"block_8": "8. blokk",
|
||||
"block_9": "9. blokk",
|
||||
"book_interview": "Interjú foglalása",
|
||||
"build_product_roadmap_description": "A felhasználók által leginkább igényelt EGY dolog azonosítása és összeállítása.",
|
||||
"build_product_roadmap_name": "Termékútiterv összeállítása",
|
||||
@@ -2452,7 +2476,7 @@
|
||||
"career_development_survey_question_6_choice_2": "Igazgató",
|
||||
"career_development_survey_question_6_choice_3": "Vezető igazgató",
|
||||
"career_development_survey_question_6_choice_4": "Alelnök",
|
||||
"career_development_survey_question_6_choice_5": "Igazgató",
|
||||
"career_development_survey_question_6_choice_5": "Ügyvezető",
|
||||
"career_development_survey_question_6_choice_6": "Egyéb",
|
||||
"career_development_survey_question_6_headline": "Az alábbiak közül melyik írja le legjobban a jelenlegi munkája szintjét?",
|
||||
"career_development_survey_question_6_subheader": "Válassza ki a következő lehetőségek egyikét:",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Jaj, bocsánat! Tehetünk valamit, amivel javíthatnánk az élményén?",
|
||||
"csat_survey_question_3_placeholder": "Írja be ide a válaszát…",
|
||||
"cta_description": "Információk megjelenítése és a felhasználók felkérése egy bizonyos művelet elvégzésére",
|
||||
"custom_survey_block_1_name": "1. blokk",
|
||||
"custom_survey_description": "Kérdőív létrehozása sablon nélkül.",
|
||||
"custom_survey_name": "Kezdés a semmiből",
|
||||
"custom_survey_question_1_headline": "Mit szeretne tudni?",
|
||||
@@ -2965,7 +2988,7 @@
|
||||
"onboarding_segmentation": "Beléptetés szakaszolása",
|
||||
"onboarding_segmentation_description": "További információk azzal kapcsolatban, hogy kik regisztráltak a termékére és miért.",
|
||||
"onboarding_segmentation_question_1_choice_1": "Alapító",
|
||||
"onboarding_segmentation_question_1_choice_2": "Igazgató",
|
||||
"onboarding_segmentation_question_1_choice_2": "Ügyvezető",
|
||||
"onboarding_segmentation_question_1_choice_3": "Termékmenedzser",
|
||||
"onboarding_segmentation_question_1_choice_4": "Terméktulajdonos",
|
||||
"onboarding_segmentation_question_1_choice_5": "Szoftvermérnök",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nem, köszönöm!",
|
||||
"preview_survey_question_2_headline": "Szeretne naprakész maradni?",
|
||||
"preview_survey_question_2_subheader": "Ez egy példa a leírásra.",
|
||||
"preview_survey_question_open_text_headline": "Bármi egyéb, amit meg szeretne osztani?",
|
||||
"preview_survey_question_open_text_placeholder": "Írja be ide a válaszát…",
|
||||
"preview_survey_question_open_text_subheader": "A visszajelzése segít nekünk a fejlődésben.",
|
||||
"preview_survey_welcome_card_headline": "Üdvözöljük!",
|
||||
"prioritize_features_description": "A felhasználóknak leginkább és legkevésbé szükséges funkciók azonosítása.",
|
||||
"prioritize_features_name": "Funkciók rangsorolása",
|
||||
@@ -3033,7 +3059,7 @@
|
||||
"product_market_fit_superhuman_question_2_headline": "Mennyire lenne csalódott, ha többé nem használhatná a(z) $[projectName] projektet?",
|
||||
"product_market_fit_superhuman_question_2_subheader": "Válassza ki a következő lehetőségek egyikét:",
|
||||
"product_market_fit_superhuman_question_3_choice_1": "Alapító",
|
||||
"product_market_fit_superhuman_question_3_choice_2": "Igazgató",
|
||||
"product_market_fit_superhuman_question_3_choice_2": "Ügyvezető",
|
||||
"product_market_fit_superhuman_question_3_choice_3": "Termékmenedzser",
|
||||
"product_market_fit_superhuman_question_3_choice_4": "Terméktulajdonos",
|
||||
"product_market_fit_superhuman_question_3_choice_5": "Szoftvermérnök",
|
||||
|
||||
+38
-12
@@ -175,9 +175,12 @@
|
||||
"copy": "コピー",
|
||||
"copy_code": "コードをコピー",
|
||||
"copy_link": "リンクをコピー",
|
||||
"count_attributes": "{value, plural, other {{value}個の属性}}",
|
||||
"count_contacts": "{count, plural, other {# 件の連絡先}}",
|
||||
"count_attributes": "{count, plural, other {{count}個の属性}}",
|
||||
"count_contacts": "{count, plural, other {{count}件の連絡先}}",
|
||||
"count_members": "{count, plural, other {{count}人のメンバー}}",
|
||||
"count_questions": "{count, plural, other {# 件の質問}}",
|
||||
"count_responses": "{count, plural, other {# 件の回答}}",
|
||||
"count_selections": "{count, plural, other {{count}件選択中}}",
|
||||
"create_new_organization": "新しい組織を作成",
|
||||
"create_segment": "セグメントを作成",
|
||||
"create_survey": "フォームを作成",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "日",
|
||||
"default": "デフォルト",
|
||||
"delete": "削除",
|
||||
"delete_what": "{deleteWhat}を削除",
|
||||
"description": "説明",
|
||||
"dev_env": "開発環境",
|
||||
"development": "開発",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "ダウンロード",
|
||||
"draft": "下書き",
|
||||
"duplicate": "複製",
|
||||
"duplicate_copy": "(コピー)",
|
||||
"duplicate_copy_number": "(コピー {copyNumber})",
|
||||
"e_commerce": "Eコマース",
|
||||
"edit": "編集",
|
||||
"email": "メールアドレス",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "ワークスペースの読み込みに失敗しました",
|
||||
"filter": "フィルター",
|
||||
"finish": "完了",
|
||||
"first_name": "名",
|
||||
"follow_these": "こちらの手順に従って",
|
||||
"formbricks_version": "Formbricksバージョン",
|
||||
"full_name": "氏名",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "非表示フィールド",
|
||||
"hidden_fields": "非表示フィールド",
|
||||
"hide_column": "列を非表示",
|
||||
"id": "ID",
|
||||
"image": "画像",
|
||||
"images": "画像",
|
||||
"import": "インポート",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "キー",
|
||||
"label": "ラベル",
|
||||
"language": "言語",
|
||||
"last_name": "姓",
|
||||
"learn_more": "詳細を見る",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "明るいオーバーレイ",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "デザイン",
|
||||
"manage": "管理",
|
||||
"marketing": "マーケティング",
|
||||
"member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"members_and_teams": "メンバー&チーム",
|
||||
"membership_not_found": "メンバーシップが見つかりません",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "下に移動",
|
||||
"move_up": "上に移動",
|
||||
"multiple_languages": "多言語",
|
||||
"my_product": "マイプロダクト",
|
||||
"name": "名前",
|
||||
"new": "新規",
|
||||
"new_version_available": "Formbricks {version} が利用可能です。今すぐアップグレード!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "チームを選択",
|
||||
"selected": "選択済み",
|
||||
"selected_questions": "選択した質問",
|
||||
"selection": "選択",
|
||||
"selections": "選択",
|
||||
"send_test_email": "テストメールを送信",
|
||||
"session_not_found": "セッションが見つかりません",
|
||||
"settings": "設定",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "こんにちは、{userName}さん",
|
||||
"email_customization_preview_email_text": "これは、メールに表示されるロゴを確認するためのプレビューメールです。",
|
||||
"error_deleting_organization_please_try_again": "組織の削除中にエラーが発生しました。もう一度お試しください。",
|
||||
"from_your_organization": "あなたの組織から",
|
||||
"from_your_organization": "組織から{memberName}を削除",
|
||||
"invitation_sent_once_more": "招待状を再度送信しました。",
|
||||
"invite_deleted_successfully": "招待を正常に削除しました",
|
||||
"invite_expires_on": "招待は{date}に期限切れ",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "質問がスキップされた場合に表示するプレースホルダーを追加:",
|
||||
"add_hidden_field_id": "非表示フィールドIDを追加",
|
||||
"add_highlight_border": "ハイライトボーダーを追加",
|
||||
"add_highlight_border_description": "プロダクト内サーベイにのみ適用されます。",
|
||||
"add_logic": "ロジックを追加",
|
||||
"add_none_of_the_above": "\"いずれも該当しません\" を追加",
|
||||
"add_option": "オプションを追加",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "「フォームはクローズしました」メッセージを調整",
|
||||
"adjust_survey_closed_message_description": "フォームがクローズしたときに訪問者が見るメッセージを変更します。",
|
||||
"adjust_the_theme_in_the": "テーマを",
|
||||
"all_are_true": "すべてが真である",
|
||||
"all_other_answers_will_continue_to": "他のすべての回答は引き続き",
|
||||
"allow_multi_select": "複数選択を許可",
|
||||
"allow_multiple_files": "複数のファイルを許可",
|
||||
"allow_users_to_select_more_than_one_image": "ユーザーが複数の画像を選択できるようにする",
|
||||
"and_launch_surveys_in_your_website_or_app": "ウェブサイトやアプリでフォームを公開できます。",
|
||||
"animation": "アニメーション",
|
||||
"any_is_true": "いずれかが真",
|
||||
"app_survey_description": "回答を収集するために、ウェブアプリまたはウェブサイトにフォームを埋め込みます。",
|
||||
"assign": "割り当て =",
|
||||
"audience": "オーディエンス",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "フォローアップ が 更新され、 アンケートを 保存すると保存されます。",
|
||||
"follow_ups_new": "新しいフォローアップ",
|
||||
"follow_ups_upgrade_button_text": "フォローアップを有効にするためにアップグレード",
|
||||
"form_styling": "フォームのスタイル",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDKが接続されていません",
|
||||
"four_points": "4点",
|
||||
"heading": "見出し",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "オプション {choiceIndex}",
|
||||
"option_used_in_logic_error": "このオプションは質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"optional": "オプション",
|
||||
"options": "オプション",
|
||||
"options": "オプション*",
|
||||
"options_used_in_logic_bulk_error": "以下のオプションはロジックで使用されています:{questionIndexes}。まず、ロジックから削除してください。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "このフォームの個別のスタイルでテーマを上書きします。",
|
||||
"overwrite_global_waiting_time": "カスタムクールダウン期間を設定",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "質問を削除しました。",
|
||||
"question_duplicated": "質問を複製しました。",
|
||||
"question_id_updated": "質問IDを更新しました",
|
||||
"question_number": "質問 {number}",
|
||||
"question_used_in_logic_warning_text": "このブロックの要素はロジックルールで使用されていますが、本当に削除しますか?",
|
||||
"question_used_in_logic_warning_title": "ロジックの不整合",
|
||||
"question_used_in_quota": "この質問は“{quotaName}”クォータで使用されています",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "回答数の上限、リダイレクトなど。",
|
||||
"response_options": "回答オプション",
|
||||
"roundness": "丸み",
|
||||
"roundness_description": "カードの角の丸みを調整します。",
|
||||
"roundness_description": "角の丸みを調整します。",
|
||||
"row_used_in_logic_error": "この行は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"rows": "行",
|
||||
"save_and_close": "保存して閉じる",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "この無料のオープンソースフォームは閉鎖されました",
|
||||
"survey_display_settings": "フォーム表示設定",
|
||||
"survey_placement": "フォームの配置",
|
||||
"survey_styling": "フォームのスタイル",
|
||||
"survey_trigger": "フォームのトリガー",
|
||||
"switch_multi_language_on_to_get_started": "多言語機能をオンにして開始 👉",
|
||||
"target_block_not_found": "対象ブロックが見つかりません",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "クールダウン期間(アンケート全体)",
|
||||
"waiting_time_across_surveys_description": "アンケート疲れを防ぐため、このアンケートがワークスペース全体のクールダウン期間とどのように連動するかを選択してください。",
|
||||
"welcome_message": "ウェルカムメッセージ",
|
||||
"when": "条件",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "フィルターがなければ、すべてのユーザーがフォームに回答できます。",
|
||||
"you_have_not_created_a_segment_yet": "まだセグメントを作成していません",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "翻訳を使用するには、ワークスペースに2つ以上の言語を設定する必要があります。",
|
||||
"your_description_here_recall_information_with": "ここにあなたの説明。@ で情報を呼び出す",
|
||||
"your_question_here_recall_information_with": "ここにあなたの質問。@ で情報を呼び出す",
|
||||
"your_web_app": "あなたのウェブアプリ",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "開始",
|
||||
"starts_tooltip": "フォームが開始された回数。",
|
||||
"survey_reset_successfully": "フォームを正常にリセットしました!{responseCount} 件の回答と {displayCount} 件の表示が削除されました。",
|
||||
"survey_results": "{surveyName}の結果",
|
||||
"this_month": "今月",
|
||||
"this_quarter": "今四半期",
|
||||
"this_year": "今年",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "バーの塗りつぶし部分に色を付けます。",
|
||||
"advanced_styling_field_input_border_radius_description": "入力フィールドの角を丸めます。",
|
||||
"advanced_styling_field_input_font_size_description": "入力フィールド内の入力テキストのサイズを調整します。",
|
||||
"advanced_styling_field_input_height_description": "入力フィールドの最小の高さを制御します。",
|
||||
"advanced_styling_field_input_height_description": "入力欄の最小の高さを調整します。",
|
||||
"advanced_styling_field_input_padding_x_description": "左右にスペースを追加します。",
|
||||
"advanced_styling_field_input_padding_y_description": "上下にスペースを追加します。",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "プレースホルダーのヒントテキストを薄くします。",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "入力フィールドに入力されたテキストの色を設定します。",
|
||||
"advanced_styling_field_option_bg": "背景",
|
||||
"advanced_styling_field_option_bg_description": "オプション項目を塗りつぶします。",
|
||||
"advanced_styling_field_option_border": "枠線の色",
|
||||
"advanced_styling_field_option_border_description": "ラジオボタンとチェックボックスの選択肢の輪郭を設定します。",
|
||||
"advanced_styling_field_option_border_radius_description": "オプションの角を丸くします。",
|
||||
"advanced_styling_field_option_font_size_description": "オプションラベルのテキストサイズを調整します。",
|
||||
"advanced_styling_field_option_label": "ラベルの色",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "会社はビジョンと戦略の整合性をどのように改善できますか?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "ここに回答を入力してください...",
|
||||
"back": "戻る",
|
||||
"block_1": "ブロック 1",
|
||||
"block_10": "ブロック 10",
|
||||
"block_2": "ブロック 2",
|
||||
"block_3": "ブロック 3",
|
||||
"block_4": "ブロック 4",
|
||||
"block_5": "ブロック 5",
|
||||
"block_6": "ブロック 6",
|
||||
"block_7": "ブロック 7",
|
||||
"block_8": "ブロック 8",
|
||||
"block_9": "ブロック 9",
|
||||
"book_interview": "面談を予約する",
|
||||
"build_product_roadmap_description": "ユーザーが最も望んでいる「たった一つ」のものを特定し、構築する。",
|
||||
"build_product_roadmap_name": "製品ロードマップの構築",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "申し訳ありません!体験を改善するために何かできることはありますか?",
|
||||
"csat_survey_question_3_placeholder": "ここに回答を入力してください...",
|
||||
"cta_description": "情報を表示し、特定の行動を促す",
|
||||
"custom_survey_block_1_name": "ブロック1",
|
||||
"custom_survey_description": "テンプレートを使わずにアンケートを作成する。",
|
||||
"custom_survey_name": "最初から始める",
|
||||
"custom_survey_question_1_headline": "何を知りたいですか?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "いいえ、結構です!",
|
||||
"preview_survey_question_2_headline": "最新情報を知りたいですか?",
|
||||
"preview_survey_question_2_subheader": "これは説明の例です。",
|
||||
"preview_survey_question_open_text_headline": "他に共有したいことはありますか?",
|
||||
"preview_survey_question_open_text_placeholder": "ここに回答を入力してください...",
|
||||
"preview_survey_question_open_text_subheader": "あなたのフィードバックは、私たちの改善に役立ちます。",
|
||||
"preview_survey_welcome_card_headline": "ようこそ!",
|
||||
"prioritize_features_description": "ユーザーが最も必要とする機能と最も必要としない機能を特定する。",
|
||||
"prioritize_features_name": "機能の優先順位付け",
|
||||
|
||||
+39
-13
@@ -175,9 +175,12 @@
|
||||
"copy": "Kopiëren",
|
||||
"copy_code": "Kopieer code",
|
||||
"copy_link": "Kopieer link",
|
||||
"count_attributes": "{value, plural, one {{value} attribuut} other {{value} attributen}}",
|
||||
"count_contacts": "{value, plural, one {{value} contact} other {{value} contacten}}",
|
||||
"count_responses": "{value, plural, one {{value} reactie} other {{value} reacties}}",
|
||||
"count_attributes": "{count, plural, one {{count} attribuut} other {{count} attributen}}",
|
||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacten}}",
|
||||
"count_members": "{count, plural, one {{count} lid} other {{count} leden}}",
|
||||
"count_questions": "{count, plural, one {{count} vraag} other {{count} vragen}}",
|
||||
"count_responses": "{count, plural, one {{count} reactie} other {{count} reacties}}",
|
||||
"count_selections": "{count, plural, one {{count} selectie} other {{count} selecties}}",
|
||||
"create_new_organization": "Creëer een nieuwe organisatie",
|
||||
"create_segment": "Segment maken",
|
||||
"create_survey": "Enquête maken",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "dagen",
|
||||
"default": "Standaard",
|
||||
"delete": "Verwijderen",
|
||||
"delete_what": "Verwijder {deleteWhat}",
|
||||
"description": "Beschrijving",
|
||||
"dev_env": "Ontwikkelomgeving",
|
||||
"development": "Ontwikkeling",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Downloaden",
|
||||
"draft": "Voorlopige versie",
|
||||
"duplicate": "Duplicaat",
|
||||
"duplicate_copy": "(kopie)",
|
||||
"duplicate_copy_number": "(kopie {copyNumber})",
|
||||
"e_commerce": "E-commerce",
|
||||
"edit": "Bewerking",
|
||||
"email": "E-mail",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Laden van werkruimtes mislukt",
|
||||
"filter": "Filter",
|
||||
"finish": "Finish",
|
||||
"first_name": "Voornaam",
|
||||
"follow_these": "Volg deze",
|
||||
"formbricks_version": "Formbricks-versie",
|
||||
"full_name": "Volledige naam",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Verborgen veld",
|
||||
"hidden_fields": "Verborgen velden",
|
||||
"hide_column": "Kolom verbergen",
|
||||
"id": "ID",
|
||||
"image": "Afbeelding",
|
||||
"images": "Afbeeldingen",
|
||||
"import": "Importeren",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Sleutel",
|
||||
"label": "Label",
|
||||
"language": "Taal",
|
||||
"last_name": "Achternaam",
|
||||
"learn_more": "Meer informatie",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Lichte overlay",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Kijk & voel",
|
||||
"manage": "Beheren",
|
||||
"marketing": "Marketing",
|
||||
"member": "Lid",
|
||||
"members": "Leden",
|
||||
"members_and_teams": "Leden & teams",
|
||||
"membership_not_found": "Lidmaatschap niet gevonden",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Ga naar beneden",
|
||||
"move_up": "Ga omhoog",
|
||||
"multiple_languages": "Meerdere talen",
|
||||
"my_product": "mijn product",
|
||||
"name": "Naam",
|
||||
"new": "Nieuw",
|
||||
"new_version_available": "Formbricks {version} is hier. Upgrade nu!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Selecteer teams",
|
||||
"selected": "Gekozen",
|
||||
"selected_questions": "Geselecteerde vragen",
|
||||
"selection": "Selectie",
|
||||
"selections": "Selecties",
|
||||
"send_test_email": "Test-e-mail verzenden",
|
||||
"session_not_found": "Sessie niet gevonden",
|
||||
"settings": "Instellingen",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Hé {userName}",
|
||||
"email_customization_preview_email_text": "Dit is een e-mailvoorbeeld om u te laten zien welk logo in de e-mails wordt weergegeven.",
|
||||
"error_deleting_organization_please_try_again": "Fout bij verwijderen van organisatie. Probeer het opnieuw.",
|
||||
"from_your_organization": "vanuit uw organisatie",
|
||||
"from_your_organization": "{memberName} uit je organisatie",
|
||||
"invitation_sent_once_more": "Uitnodiging nogmaals verzonden.",
|
||||
"invite_deleted_successfully": "Uitnodiging succesvol verwijderd",
|
||||
"invite_expires_on": "Uitnodiging verloopt op {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Voeg een tijdelijke aanduiding toe om aan te geven of er geen waarde is om te onthouden.",
|
||||
"add_hidden_field_id": "Voeg een verborgen veld-ID toe",
|
||||
"add_highlight_border": "Markeerrand toevoegen",
|
||||
"add_highlight_border_description": "Geldt alleen voor in-product enquêtes.",
|
||||
"add_logic": "Voeg logica toe",
|
||||
"add_none_of_the_above": "Voeg 'Geen van bovenstaande' toe",
|
||||
"add_option": "Optie toevoegen",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Pas het bericht 'Enquête gesloten' aan",
|
||||
"adjust_survey_closed_message_description": "Wijzig het bericht dat bezoekers zien wanneer de enquête wordt gesloten.",
|
||||
"adjust_the_theme_in_the": "Pas het thema aan in de",
|
||||
"all_are_true": "alle zijn waar",
|
||||
"all_other_answers_will_continue_to": "Alle andere antwoorden blijven hetzelfde",
|
||||
"allow_multi_select": "Multi-select toestaan",
|
||||
"allow_multiple_files": "Meerdere bestanden toestaan",
|
||||
"allow_users_to_select_more_than_one_image": "Sta gebruikers toe meer dan één afbeelding te selecteren",
|
||||
"and_launch_surveys_in_your_website_or_app": "en start enquêtes op uw website of app.",
|
||||
"animation": "Animatie",
|
||||
"any_is_true": "een is waar",
|
||||
"app_survey_description": "Sluit een enquête in uw web-app of website in om reacties te verzamelen.",
|
||||
"assign": "Toewijzen =",
|
||||
"audience": "Publiek",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Follow-up bijgewerkt en wordt opgeslagen zodra u de enquête opslaat.",
|
||||
"follow_ups_new": "Nieuw vervolg",
|
||||
"follow_ups_upgrade_button_text": "Upgrade om follow-ups mogelijk te maken",
|
||||
"form_styling": "Vorm styling",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDK is niet verbonden",
|
||||
"four_points": "4 punten",
|
||||
"heading": "Rubriek",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Optie {choiceIndex}",
|
||||
"option_used_in_logic_error": "Deze optie wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
||||
"optional": "Optioneel",
|
||||
"options": "Opties",
|
||||
"options": "Opties*",
|
||||
"options_used_in_logic_bulk_error": "De volgende opties worden gebruikt in logica: {questionIndexes}. Verwijder ze eerst uit de logica.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Overschrijf het thema met individuele stijlen voor deze enquête.",
|
||||
"overwrite_global_waiting_time": "Aangepaste afkoelperiode instellen",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Vraag verwijderd.",
|
||||
"question_duplicated": "Vraag dubbel gesteld.",
|
||||
"question_id_updated": "Vraag-ID bijgewerkt",
|
||||
"question_number": "Vraag {number}",
|
||||
"question_used_in_logic_warning_text": "Elementen uit dit blok worden gebruikt in een logische regel, weet je zeker dat je het wilt verwijderen?",
|
||||
"question_used_in_logic_warning_title": "Logica-inconsistentie",
|
||||
"question_used_in_quota": "Deze vraag wordt gebruikt in het quotum “{quotaName}”",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Reactielimieten, omleidingen en meer.",
|
||||
"response_options": "Reactieopties",
|
||||
"roundness": "Rondheid",
|
||||
"roundness_description": "Bepaalt hoe afgerond de kaarthoeken zijn.",
|
||||
"roundness_description": "Bepaalt hoe afgerond de hoeken zijn.",
|
||||
"row_used_in_logic_error": "Deze rij wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
||||
"rows": "Rijen",
|
||||
"save_and_close": "Opslaan en sluiten",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Deze gratis en open source-enquête is gesloten",
|
||||
"survey_display_settings": "Enquêteweergave-instellingen",
|
||||
"survey_placement": "Enquête plaatsing",
|
||||
"survey_styling": "Vorm styling",
|
||||
"survey_trigger": "Enquêtetrigger",
|
||||
"switch_multi_language_on_to_get_started": "Schakel meertaligheid in om te beginnen 👉",
|
||||
"target_block_not_found": "Doelblok niet gevonden",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Afkoelperiode (voor alle enquêtes)",
|
||||
"waiting_time_across_surveys_description": "Om enquêtemoeheid te voorkomen, kies hoe deze enquête omgaat met de workspace-brede afkoelperiode.",
|
||||
"welcome_message": "Welkomstbericht",
|
||||
"when": "Wanneer",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Zonder filter kunnen al uw gebruikers worden bevraagd.",
|
||||
"you_have_not_created_a_segment_yet": "U heeft nog geen segment aangemaakt",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Je moet twee of meer talen hebben ingesteld in je werkruimte om met vertalingen te kunnen werken.",
|
||||
"your_description_here_recall_information_with": "Uw beschrijving hier. Roep informatie op met @",
|
||||
"your_question_here_recall_information_with": "Uw vraag hier. Roep informatie op met @",
|
||||
"your_web_app": "Uw web-app",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Begint",
|
||||
"starts_tooltip": "Aantal keren dat de enquête is gestart.",
|
||||
"survey_reset_successfully": "Enquête opnieuw ingesteld! {responseCount} reacties en {displayCount} displays zijn verwijderd.",
|
||||
"survey_results": "Resultaten van {surveyName}",
|
||||
"this_month": "Deze maand",
|
||||
"this_quarter": "Dit kwartaal",
|
||||
"this_year": "Dit jaar",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Kleurt het gevulde deel van de balk.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rondt de invoerhoeken af.",
|
||||
"advanced_styling_field_input_font_size_description": "Schaalt de getypte tekst in invoervelden.",
|
||||
"advanced_styling_field_input_height_description": "Bepaalt de minimale hoogte van het invoerveld.",
|
||||
"advanced_styling_field_input_height_description": "Bepaalt de min. hoogte van het invoerveld.",
|
||||
"advanced_styling_field_input_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
|
||||
"advanced_styling_field_input_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Vervaagt de tijdelijke aanwijzingstekst.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Kleurt de getypte tekst in invoervelden.",
|
||||
"advanced_styling_field_option_bg": "Achtergrond",
|
||||
"advanced_styling_field_option_bg_description": "Vult de optie-items.",
|
||||
"advanced_styling_field_option_border": "Randkleur",
|
||||
"advanced_styling_field_option_border_description": "Omlijnt radio- en checkboxopties.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rondt de hoeken van opties af.",
|
||||
"advanced_styling_field_option_font_size_description": "Schaalt de tekst van optielabels.",
|
||||
"advanced_styling_field_option_label": "Labelkleur",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Hoe kan het bedrijf de afstemming van zijn visie en strategie verbeteren?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Typ hier uw antwoord...",
|
||||
"back": "Rug",
|
||||
"block_1": "Blok 1",
|
||||
"block_10": "Blok 10",
|
||||
"block_2": "Blok 2",
|
||||
"block_3": "Blok 3",
|
||||
"block_4": "Blok 4",
|
||||
"block_5": "Blok 5",
|
||||
"block_6": "Blok 6",
|
||||
"block_7": "Blok 7",
|
||||
"block_8": "Blok 8",
|
||||
"block_9": "Blok 9",
|
||||
"book_interview": "Boek interview",
|
||||
"build_product_roadmap_description": "Identificeer het ENE wat uw gebruikers het liefst willen en bouw het.",
|
||||
"build_product_roadmap_name": "Productroadmap opstellen",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Euh, sorry! Kunnen we iets doen om uw ervaring te verbeteren?",
|
||||
"csat_survey_question_3_placeholder": "Typ hier uw antwoord...",
|
||||
"cta_description": "Geef informatie weer en vraag gebruikers om een specifieke actie te ondernemen",
|
||||
"custom_survey_block_1_name": "Blok 1",
|
||||
"custom_survey_description": "Maak een enquête zonder sjabloon.",
|
||||
"custom_survey_name": "Begin helemaal opnieuw",
|
||||
"custom_survey_question_1_headline": "Wat zou je willen weten?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nee, dank je!",
|
||||
"preview_survey_question_2_headline": "Wil je op de hoogte blijven?",
|
||||
"preview_survey_question_2_subheader": "Dit is een voorbeeldbeschrijving.",
|
||||
"preview_survey_question_open_text_headline": "Wil je nog iets delen?",
|
||||
"preview_survey_question_open_text_placeholder": "Typ hier je antwoord...",
|
||||
"preview_survey_question_open_text_subheader": "Je feedback helpt ons verbeteren.",
|
||||
"preview_survey_welcome_card_headline": "Welkom!",
|
||||
"prioritize_features_description": "Identificeer functies die uw gebruikers het meest en het minst nodig hebben.",
|
||||
"prioritize_features_name": "Geef prioriteit aan functies",
|
||||
|
||||
+39
-13
@@ -175,9 +175,12 @@
|
||||
"copy": "Copiar",
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"count_attributes": "{value, plural, one {{value} atributo} other {{value} atributos}}",
|
||||
"count_contacts": "{value, plural, one {# contato} other {# contatos} }",
|
||||
"count_responses": "{value, plural, other {# respostas}}",
|
||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||
"count_contacts": "{count, plural, one {{count} contato} other {{count} contatos}}",
|
||||
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
||||
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
|
||||
"count_responses": "{count, plural, other {# respostas}}",
|
||||
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
"create_segment": "Criar segmento",
|
||||
"create_survey": "Criar pesquisa",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "dias",
|
||||
"default": "Padrão",
|
||||
"delete": "Apagar",
|
||||
"delete_what": "Excluir {deleteWhat}",
|
||||
"description": "Descrição",
|
||||
"dev_env": "Ambiente de Desenvolvimento",
|
||||
"development": "Desenvolvimento",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "baixar",
|
||||
"draft": "Rascunho",
|
||||
"duplicate": "Duplicar",
|
||||
"duplicate_copy": "(cópia)",
|
||||
"duplicate_copy_number": "(cópia {copyNumber})",
|
||||
"e_commerce": "comércio eletrônico",
|
||||
"edit": "Editar",
|
||||
"email": "Email",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Falha ao carregar projetos",
|
||||
"filter": "Filtro",
|
||||
"finish": "Terminar",
|
||||
"first_name": "Primeiro nome",
|
||||
"follow_these": "Siga esses",
|
||||
"formbricks_version": "Versão do Formbricks",
|
||||
"full_name": "Nome completo",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Campo oculto",
|
||||
"hidden_fields": "Campos ocultos",
|
||||
"hide_column": "Ocultar coluna",
|
||||
"id": "ID",
|
||||
"image": "imagem",
|
||||
"images": "Imagens",
|
||||
"import": "importar",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Chave",
|
||||
"label": "Etiqueta",
|
||||
"language": "Língua",
|
||||
"last_name": "Sobrenome",
|
||||
"learn_more": "Saiba mais",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "sobreposição leve",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Aparência e Experiência",
|
||||
"manage": "gerenciar",
|
||||
"marketing": "marketing",
|
||||
"member": "Membros",
|
||||
"members": "Membros",
|
||||
"members_and_teams": "Membros e equipes",
|
||||
"membership_not_found": "Assinatura não encontrada",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Descer",
|
||||
"move_up": "Subir",
|
||||
"multiple_languages": "Vários idiomas",
|
||||
"my_product": "meu produto",
|
||||
"name": "Nome",
|
||||
"new": "Novo",
|
||||
"new_version_available": "Formbricks {version} chegou. Atualize agora!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Selecionar times",
|
||||
"selected": "Selecionado",
|
||||
"selected_questions": "Perguntas selecionadas",
|
||||
"selection": "seleção",
|
||||
"selections": "seleções",
|
||||
"send_test_email": "Enviar e-mail de teste",
|
||||
"session_not_found": "Sessão não encontrada",
|
||||
"settings": "Configurações",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Oi {userName}",
|
||||
"email_customization_preview_email_text": "Esta é uma pré-visualização de e-mail para mostrar qual logo será renderizado nos e-mails.",
|
||||
"error_deleting_organization_please_try_again": "Erro ao deletar a organização. Por favor, tente novamente.",
|
||||
"from_your_organization": "da sua organização",
|
||||
"from_your_organization": "{memberName} da sua organização",
|
||||
"invitation_sent_once_more": "Convite enviado de novo.",
|
||||
"invite_deleted_successfully": "Convite deletado com sucesso",
|
||||
"invite_expires_on": "O convite expira em {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Adicionar um texto padrão para mostrar se a pergunta for ignorada:",
|
||||
"add_hidden_field_id": "Adicionar campo oculto ID",
|
||||
"add_highlight_border": "Adicionar borda de destaque",
|
||||
"add_highlight_border_description": "Aplica-se apenas a pesquisas no produto.",
|
||||
"add_logic": "Adicionar lógica",
|
||||
"add_none_of_the_above": "Adicionar \"Nenhuma das opções acima\"",
|
||||
"add_option": "Adicionar opção",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensagem 'Pesquisa Encerrada''",
|
||||
"adjust_survey_closed_message_description": "Mude a mensagem que os visitantes veem quando a pesquisa está fechada.",
|
||||
"adjust_the_theme_in_the": "Ajuste o tema no",
|
||||
"all_are_true": "todas são verdadeiras",
|
||||
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
|
||||
"allow_multi_select": "Permitir seleção múltipla",
|
||||
"allow_multiple_files": "Permitir vários arquivos",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir que os usuários selecionem mais de uma imagem",
|
||||
"and_launch_surveys_in_your_website_or_app": "e lançar pesquisas no seu site ou app.",
|
||||
"animation": "animação",
|
||||
"any_is_true": "qualquer uma é verdadeira",
|
||||
"app_survey_description": "Embuta uma pesquisa no seu app ou site para coletar respostas.",
|
||||
"assign": "atribuir =",
|
||||
"audience": "Público",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Acompanhamento atualizado e será salvo assim que você salvar a pesquisa.",
|
||||
"follow_ups_new": "Novo acompanhamento",
|
||||
"follow_ups_upgrade_button_text": "Atualize para habilitar os Acompanhamentos",
|
||||
"form_styling": "Estilização de Formulários",
|
||||
"formbricks_sdk_is_not_connected": "O SDK do Formbricks não está conectado",
|
||||
"four_points": "4 pontos",
|
||||
"heading": "Título",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Opção {choiceIndex}",
|
||||
"option_used_in_logic_error": "Esta opção é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"options": "Opções*",
|
||||
"options_used_in_logic_bulk_error": "As seguintes opções são usadas na lógica: {questionIndexes}. Por favor, remova-as da lógica primeiro.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substitua o tema com estilos individuais para essa pesquisa.",
|
||||
"overwrite_global_waiting_time": "Definir período de espera personalizado",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Pergunta deletada.",
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
"question_number": "Pergunta {number}",
|
||||
"question_used_in_logic_warning_text": "Elementos deste bloco são usados em uma regra de lógica, tem certeza de que deseja excluí-lo?",
|
||||
"question_used_in_logic_warning_title": "Inconsistência de lógica",
|
||||
"question_used_in_quota": "Esta pergunta está sendo usada na cota \"{quotaName}\"",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||
"response_options": "Opções de Resposta",
|
||||
"roundness": "Circularidade",
|
||||
"roundness_description": "Controla o arredondamento dos cantos do cartão.",
|
||||
"roundness_description": "Controla o arredondamento dos cantos.",
|
||||
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"rows": "linhas",
|
||||
"save_and_close": "Salvar e Fechar",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Essa pesquisa gratuita e de código aberto foi encerrada",
|
||||
"survey_display_settings": "Configurações de Exibição da Pesquisa",
|
||||
"survey_placement": "Posicionamento da Pesquisa",
|
||||
"survey_styling": "Estilização de Formulários",
|
||||
"survey_trigger": "Gatilho de Pesquisa",
|
||||
"switch_multi_language_on_to_get_started": "Ative o modo multilíngue para começar 👉",
|
||||
"target_block_not_found": "Bloco de destino não encontrado",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Período de espera (entre pesquisas)",
|
||||
"waiting_time_across_surveys_description": "Para evitar fadiga de pesquisas, escolha como esta pesquisa interage com o período de espera geral do workspace.",
|
||||
"welcome_message": "Mensagem de boas-vindas",
|
||||
"when": "Quando",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus usuários podem ser pesquisados.",
|
||||
"you_have_not_created_a_segment_yet": "Você ainda não criou um segmento.",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Você precisa ter dois ou mais idiomas configurados em seu espaço de trabalho para trabalhar com traduções.",
|
||||
"your_description_here_recall_information_with": "Sua descrição aqui. Lembre-se de informações com @",
|
||||
"your_question_here_recall_information_with": "Sua pergunta aqui. Lembre-se de informações com @",
|
||||
"your_web_app": "Sua aplicação web",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "começa",
|
||||
"starts_tooltip": "Número de vezes que a pesquisa foi iniciada.",
|
||||
"survey_reset_successfully": "Pesquisa redefinida com sucesso! {responseCount} respostas e {displayCount} exibições foram deletadas.",
|
||||
"survey_results": "Resultados de {surveyName}",
|
||||
"this_month": "Este mês",
|
||||
"this_quarter": "Este trimestre",
|
||||
"this_year": "Este ano",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
|
||||
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
|
||||
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
|
||||
"advanced_styling_field_input_height_description": "Controla a altura mínima do campo de entrada.",
|
||||
"advanced_styling_field_input_height_description": "Controla a altura mínima da entrada.",
|
||||
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_input_padding_y_description": "Adiciona espaço na parte superior e inferior.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Esmaece o texto de dica do placeholder.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Colore o texto digitado nos campos de entrada.",
|
||||
"advanced_styling_field_option_bg": "Fundo",
|
||||
"advanced_styling_field_option_bg_description": "Preenche os itens de opção.",
|
||||
"advanced_styling_field_option_border": "Cor da borda",
|
||||
"advanced_styling_field_option_border_description": "Contorna as opções de botões de rádio e caixas de seleção.",
|
||||
"advanced_styling_field_option_border_radius_description": "Arredonda os cantos das opções.",
|
||||
"advanced_styling_field_option_font_size_description": "Ajusta o tamanho do texto do rótulo da opção.",
|
||||
"advanced_styling_field_option_label": "Cor do rótulo",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Como a empresa pode melhorar sua visão e direcionamento estratégico?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Digite sua resposta aqui...",
|
||||
"back": "voltar",
|
||||
"block_1": "Bloco 1",
|
||||
"block_10": "Bloco 10",
|
||||
"block_2": "Bloco 2",
|
||||
"block_3": "Bloco 3",
|
||||
"block_4": "Bloco 4",
|
||||
"block_5": "Bloco 5",
|
||||
"block_6": "Bloco 6",
|
||||
"block_7": "Bloco 7",
|
||||
"block_8": "Bloco 8",
|
||||
"block_9": "Bloco 9",
|
||||
"book_interview": "Marcar entrevista",
|
||||
"build_product_roadmap_description": "Identifique a ÚNICA coisa que seus usuários mais querem e construa isso.",
|
||||
"build_product_roadmap_name": "Construir Roteiro do Produto",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ah, foi mal! Tem algo que a gente possa fazer pra melhorar sua experiência?",
|
||||
"csat_survey_question_3_placeholder": "Digite sua resposta aqui...",
|
||||
"cta_description": "Mostrar informações e pedir para os usuários tomarem uma ação específica",
|
||||
"custom_survey_block_1_name": "Bloco 1",
|
||||
"custom_survey_description": "Crie uma pesquisa sem modelo.",
|
||||
"custom_survey_name": "Começar do zero",
|
||||
"custom_survey_question_1_headline": "O que você gostaria de saber?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||
"preview_survey_question_2_headline": "Quer ficar por dentro?",
|
||||
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
||||
"preview_survey_question_open_text_headline": "Tem mais alguma coisa que você gostaria de compartilhar?",
|
||||
"preview_survey_question_open_text_placeholder": "Digite sua resposta aqui...",
|
||||
"preview_survey_question_open_text_subheader": "Seu feedback nos ajuda a melhorar.",
|
||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||
"prioritize_features_description": "Identifique os recursos que seus usuários mais e menos precisam.",
|
||||
"prioritize_features_name": "Priorizar Funcionalidades",
|
||||
|
||||
+39
-13
@@ -175,9 +175,12 @@
|
||||
"copy": "Copiar",
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"count_attributes": "{value, plural, one {{value} atributo} other {{value} atributos}}",
|
||||
"count_contacts": "{value, plural, one {# contacto} other {# contactos} }",
|
||||
"count_responses": "{value, plural, other {# respostas}}",
|
||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
||||
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
||||
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
|
||||
"count_responses": "{count, plural, other {# respostas}}",
|
||||
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
"create_segment": "Criar segmento",
|
||||
"create_survey": "Criar inquérito",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "dias",
|
||||
"default": "Padrão",
|
||||
"delete": "Eliminar",
|
||||
"delete_what": "Eliminar {deleteWhat}",
|
||||
"description": "Descrição",
|
||||
"dev_env": "Ambiente de Desenvolvimento",
|
||||
"development": "Desenvolvimento",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Transferir",
|
||||
"draft": "Rascunho",
|
||||
"duplicate": "Duplicar",
|
||||
"duplicate_copy": "(cópia)",
|
||||
"duplicate_copy_number": "(cópia {copyNumber})",
|
||||
"e_commerce": "Comércio Eletrónico",
|
||||
"edit": "Editar",
|
||||
"email": "Email",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Falha ao carregar projetos",
|
||||
"filter": "Filtro",
|
||||
"finish": "Concluir",
|
||||
"first_name": "Primeiro nome",
|
||||
"follow_these": "Siga estes",
|
||||
"formbricks_version": "Versão do Formbricks",
|
||||
"full_name": "Nome completo",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Campo oculto",
|
||||
"hidden_fields": "Campos ocultos",
|
||||
"hide_column": "Ocultar coluna",
|
||||
"id": "ID",
|
||||
"image": "Imagem",
|
||||
"images": "Imagens",
|
||||
"import": "Importar",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Chave",
|
||||
"label": "Etiqueta",
|
||||
"language": "Idioma",
|
||||
"last_name": "Apelido",
|
||||
"learn_more": "Saiba mais",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Sobreposição leve",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Aparência e Sensação",
|
||||
"manage": "Gerir",
|
||||
"marketing": "Marketing",
|
||||
"member": "Membro",
|
||||
"members": "Membros",
|
||||
"members_and_teams": "Membros e equipas",
|
||||
"membership_not_found": "Associação não encontrada",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Mover para baixo",
|
||||
"move_up": "Mover para cima",
|
||||
"multiple_languages": "Várias línguas",
|
||||
"my_product": "o meu produto",
|
||||
"name": "Nome",
|
||||
"new": "Novo",
|
||||
"new_version_available": "Formbricks {version} está aqui. Atualize agora!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Selecionar equipas",
|
||||
"selected": "Selecionado",
|
||||
"selected_questions": "Perguntas selecionadas",
|
||||
"selection": "Seleção",
|
||||
"selections": "Seleções",
|
||||
"send_test_email": "Enviar email de teste",
|
||||
"session_not_found": "Sessão não encontrada",
|
||||
"settings": "Configurações",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Olá {userName}",
|
||||
"email_customization_preview_email_text": "Esta é uma pré-visualização de email para mostrar qual logotipo será exibido nos emails.",
|
||||
"error_deleting_organization_please_try_again": "Erro ao eliminar a organização. Por favor, tente novamente.",
|
||||
"from_your_organization": "da sua organização",
|
||||
"from_your_organization": "{memberName} da sua organização",
|
||||
"invitation_sent_once_more": "Convite enviado mais uma vez.",
|
||||
"invite_deleted_successfully": "Convite eliminado com sucesso",
|
||||
"invite_expires_on": "O convite expira em {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Adicionar um espaço reservado para mostrar se não houver valor para recordar.",
|
||||
"add_hidden_field_id": "Adicionar ID do campo oculto",
|
||||
"add_highlight_border": "Adicionar borda de destaque",
|
||||
"add_highlight_border_description": "Aplica-se apenas a inquéritos no produto.",
|
||||
"add_logic": "Adicionar lógica",
|
||||
"add_none_of_the_above": "Adicionar \"Nenhuma das Opções Acima\"",
|
||||
"add_option": "Adicionar opção",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensagem de 'Inquérito Fechado'",
|
||||
"adjust_survey_closed_message_description": "Alterar a mensagem que os visitantes veem quando o inquérito está fechado.",
|
||||
"adjust_the_theme_in_the": "Ajustar o tema no",
|
||||
"all_are_true": "todas são verdadeiras",
|
||||
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
|
||||
"allow_multi_select": "Permitir seleção múltipla",
|
||||
"allow_multiple_files": "Permitir vários ficheiros",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir aos utilizadores selecionar mais do que uma imagem",
|
||||
"and_launch_surveys_in_your_website_or_app": "e lance inquéritos no seu site ou aplicação.",
|
||||
"animation": "Animação",
|
||||
"any_is_true": "qualquer uma é verdadeira",
|
||||
"app_survey_description": "Incorpore um inquérito na sua aplicação web ou site para recolher respostas.",
|
||||
"assign": "Atribuir =",
|
||||
"audience": "Público",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Seguimento atualizado e será guardado assim que guardar o questionário.",
|
||||
"follow_ups_new": "Novo acompanhamento",
|
||||
"follow_ups_upgrade_button_text": "Atualize para ativar os acompanhamentos",
|
||||
"form_styling": "Estilo do formulário",
|
||||
"formbricks_sdk_is_not_connected": "O SDK do Formbricks não está conectado",
|
||||
"four_points": "4 pontos",
|
||||
"heading": "Cabeçalho",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Opção {choiceIndex}",
|
||||
"option_used_in_logic_error": "Esta opção é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"options": "Opções*",
|
||||
"options_used_in_logic_bulk_error": "As seguintes opções são usadas na lógica: {questionIndexes}. Por favor, remova-as da lógica primeiro.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substituir o tema com estilos individuais para este inquérito.",
|
||||
"overwrite_global_waiting_time": "Definir período de espera personalizado",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Pergunta eliminada.",
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
"question_number": "Pergunta {number}",
|
||||
"question_used_in_logic_warning_text": "Os elementos deste bloco são utilizados numa regra de lógica, tem a certeza de que pretende eliminá-lo?",
|
||||
"question_used_in_logic_warning_title": "Inconsistência de lógica",
|
||||
"question_used_in_quota": "Esta pergunta está a ser usada na quota \"{quotaName}\"",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||
"response_options": "Opções de Resposta",
|
||||
"roundness": "Arredondamento",
|
||||
"roundness_description": "Controla o arredondamento dos cantos do cartão.",
|
||||
"roundness_description": "Controla o arredondamento dos cantos.",
|
||||
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"rows": "Linhas",
|
||||
"save_and_close": "Guardar e Fechar",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Este inquérito gratuito e de código aberto foi encerrado",
|
||||
"survey_display_settings": "Configurações de Exibição do Inquérito",
|
||||
"survey_placement": "Colocação do Inquérito",
|
||||
"survey_styling": "Estilo do formulário",
|
||||
"survey_trigger": "Desencadeador de Inquérito",
|
||||
"switch_multi_language_on_to_get_started": "Ative o modo multilingue para começar 👉",
|
||||
"target_block_not_found": "Bloco de destino não encontrado",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Período de espera (entre inquéritos)",
|
||||
"waiting_time_across_surveys_description": "Para prevenir fadiga de inquéritos, escolha como este inquérito interage com o período de espera geral do espaço de trabalho.",
|
||||
"welcome_message": "Mensagem de boas-vindas",
|
||||
"when": "Quando",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus utilizadores podem ser pesquisados.",
|
||||
"you_have_not_created_a_segment_yet": "Ainda não criou um segmento",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Precisa de ter dois ou mais idiomas configurados no seu espaço de trabalho para trabalhar com traduções.",
|
||||
"your_description_here_recall_information_with": "A sua descrição aqui. Recorde a informação com @",
|
||||
"your_question_here_recall_information_with": "A sua pergunta aqui. Recorde a informação com @",
|
||||
"your_web_app": "A sua aplicação web",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Começa",
|
||||
"starts_tooltip": "Número de vezes que o inquérito foi iniciado.",
|
||||
"survey_reset_successfully": "Inquérito reiniciado com sucesso! {responseCount} respostas e {displayCount} exibições foram eliminadas.",
|
||||
"survey_results": "Resultados de {surveyName}",
|
||||
"this_month": "Este mês",
|
||||
"this_quarter": "Este trimestre",
|
||||
"this_year": "Este ano",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
|
||||
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
|
||||
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
|
||||
"advanced_styling_field_input_height_description": "Controla a altura mínima do campo de entrada.",
|
||||
"advanced_styling_field_input_height_description": "Controla a altura mínima da entrada.",
|
||||
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_input_padding_y_description": "Adiciona espaço no topo e na base.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Atenua o texto de sugestão do placeholder.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Colore o texto digitado nos campos de entrada.",
|
||||
"advanced_styling_field_option_bg": "Fundo",
|
||||
"advanced_styling_field_option_bg_description": "Preenche os itens de opção.",
|
||||
"advanced_styling_field_option_border": "Cor do contorno",
|
||||
"advanced_styling_field_option_border_description": "Contorna as opções de botões de rádio e caixas de seleção.",
|
||||
"advanced_styling_field_option_border_radius_description": "Arredonda os cantos das opções.",
|
||||
"advanced_styling_field_option_font_size_description": "Ajusta o tamanho do texto da etiqueta da opção.",
|
||||
"advanced_styling_field_option_label": "Cor da etiqueta",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Como pode a empresa melhorar o alinhamento da sua visão e estratégia?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Escreva a sua resposta aqui...",
|
||||
"back": "Voltar",
|
||||
"block_1": "Bloco 1",
|
||||
"block_10": "Bloco 10",
|
||||
"block_2": "Bloco 2",
|
||||
"block_3": "Bloco 3",
|
||||
"block_4": "Bloco 4",
|
||||
"block_5": "Bloco 5",
|
||||
"block_6": "Bloco 6",
|
||||
"block_7": "Bloco 7",
|
||||
"block_8": "Bloco 8",
|
||||
"block_9": "Bloco 9",
|
||||
"book_interview": "Agendar entrevista",
|
||||
"build_product_roadmap_description": "Identifique a ÚNICA coisa que os seus utilizadores mais querem e construa-a.",
|
||||
"build_product_roadmap_name": "Construir Roteiro do Produto",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Oh, desculpe! Há algo que possamos fazer para melhorar a sua experiência?",
|
||||
"csat_survey_question_3_placeholder": "Escreva a sua resposta aqui...",
|
||||
"cta_description": "Exibir informações e solicitar aos utilizadores que tomem uma ação específica",
|
||||
"custom_survey_block_1_name": "Bloco 1",
|
||||
"custom_survey_description": "Crie um inquérito sem modelo.",
|
||||
"custom_survey_name": "Começar do zero",
|
||||
"custom_survey_question_1_headline": "O que gostaria de saber?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||
"preview_survey_question_2_headline": "Quer manter-se atualizado?",
|
||||
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
||||
"preview_survey_question_open_text_headline": "Mais alguma coisa que gostaria de partilhar?",
|
||||
"preview_survey_question_open_text_placeholder": "Escreva a sua resposta aqui...",
|
||||
"preview_survey_question_open_text_subheader": "O seu feedback ajuda-nos a melhorar.",
|
||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||
"prioritize_features_description": "Identifique as funcionalidades que os seus utilizadores precisam mais e menos.",
|
||||
"prioritize_features_name": "Priorizar Funcionalidades",
|
||||
|
||||
+38
-12
@@ -175,9 +175,12 @@
|
||||
"copy": "Copiază",
|
||||
"copy_code": "Copiază codul",
|
||||
"copy_link": "Copiază legătura",
|
||||
"count_attributes": "{value, plural, one {{value} atribut} few {{value} atribute} other {{value} de atribute}}",
|
||||
"count_contacts": "{value, plural, one {# contact} other {# contacte} }",
|
||||
"count_responses": "{value, plural, one {# răspuns} other {# răspunsuri} }",
|
||||
"count_attributes": "{count, plural, one {{count} atribut} few {{count} atribute} other {{count} de atribute}}",
|
||||
"count_contacts": "{count, plural, one {{count} contact} few {{count} contacte} other {{count} de contacte}}",
|
||||
"count_members": "{count, plural, one {{count} membru} few {{count} membri} other {{count} de membri}}",
|
||||
"count_questions": "{count, plural, one {# întrebare} few {# întrebări} other {# de întrebări}}",
|
||||
"count_responses": "{count, plural, one {# răspuns} other {# răspunsuri} }",
|
||||
"count_selections": "{count, plural, one {{count} selecție} few {{count} selecții} other {{count} de selecții}}",
|
||||
"create_new_organization": "Creează organizație nouă",
|
||||
"create_segment": "Creați segment",
|
||||
"create_survey": "Creează sondaj",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "zile",
|
||||
"default": "Implicit",
|
||||
"delete": "Șterge",
|
||||
"delete_what": "Șterge {deleteWhat}",
|
||||
"description": "Descriere",
|
||||
"dev_env": "Mediu de dezvoltare",
|
||||
"development": "Dezvoltare",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Descărcare",
|
||||
"draft": "Schiță",
|
||||
"duplicate": "Duplicități",
|
||||
"duplicate_copy": "(copie)",
|
||||
"duplicate_copy_number": "(copie {copyNumber})",
|
||||
"e_commerce": "Comerț electronic",
|
||||
"edit": "Editare",
|
||||
"email": "Email",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Nu s-au putut încărca workspaces",
|
||||
"filter": "Filtru",
|
||||
"finish": "Finalizează",
|
||||
"first_name": "Prenume",
|
||||
"follow_these": "Urmați acestea",
|
||||
"formbricks_version": "Versiunea Formbricks",
|
||||
"full_name": "Nume complet",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Câmp ascuns",
|
||||
"hidden_fields": "Câmpuri ascunse",
|
||||
"hide_column": "Ascunde coloana",
|
||||
"id": "ID",
|
||||
"image": "Imagine",
|
||||
"images": "Imagini",
|
||||
"import": "Import",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Cheie",
|
||||
"label": "Etichetă",
|
||||
"language": "Limba",
|
||||
"last_name": "Nume de familie",
|
||||
"learn_more": "Află mai multe",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Suprapunere ușoară",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Aspect și Comportament",
|
||||
"manage": "Gestionați",
|
||||
"marketing": "Marketing",
|
||||
"member": "Membru",
|
||||
"members": "Membri",
|
||||
"members_and_teams": "Membri și echipe",
|
||||
"membership_not_found": "Apartenența nu a fost găsită",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Mută în jos",
|
||||
"move_up": "Mută sus",
|
||||
"multiple_languages": "Mai multe limbi",
|
||||
"my_product": "produsul meu",
|
||||
"name": "Nume",
|
||||
"new": "Nou",
|
||||
"new_version_available": "Formbricks {version} este disponibil. Actualizați acum!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Selectați echipele",
|
||||
"selected": "Selectat",
|
||||
"selected_questions": "Întrebări selectate",
|
||||
"selection": "Selecție",
|
||||
"selections": "Selecții",
|
||||
"send_test_email": "Trimite email de test",
|
||||
"session_not_found": "Sesiune inexistentă",
|
||||
"settings": "Setări",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Salut {userName}",
|
||||
"email_customization_preview_email_text": "Acesta este o previzualizare a e-mailului pentru a vă arăta ce logo va fi afișat în e-mailurile.",
|
||||
"error_deleting_organization_please_try_again": "Eroare la ștergerea organizației. Vă rugăm să încercați din nou.",
|
||||
"from_your_organization": "din organizația ta",
|
||||
"from_your_organization": "{memberName} din organizația ta",
|
||||
"invitation_sent_once_more": "Invitație trimisă din nou.",
|
||||
"invite_deleted_successfully": "Invitație ștearsă cu succes",
|
||||
"invite_expires_on": "Invitația expiră pe {date}",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Adaugă un placeholder pentru a afișa dacă nu există valoare de reamintit",
|
||||
"add_hidden_field_id": "Adăugați ID câmp ascuns",
|
||||
"add_highlight_border": "Adaugă bordură evidențiată",
|
||||
"add_highlight_border_description": "Se aplică doar sondajelor din produs.",
|
||||
"add_logic": "Adaugă logică",
|
||||
"add_none_of_the_above": "Adăugați \"Niciuna dintre cele de mai sus\"",
|
||||
"add_option": "Adăugați opțiune",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustați mesajul 'Sondaj Închis'",
|
||||
"adjust_survey_closed_message_description": "Schimbați mesajul pe care îl văd vizitatorii atunci când sondajul este închis.",
|
||||
"adjust_the_theme_in_the": "Ajustați tema în",
|
||||
"all_are_true": "toate sunt adevărate",
|
||||
"all_other_answers_will_continue_to": "Toate celelalte răspunsuri vor continua să",
|
||||
"allow_multi_select": "Permite selectare multiplă",
|
||||
"allow_multiple_files": "Permite fișiere multiple",
|
||||
"allow_users_to_select_more_than_one_image": "Permite utilizatorilor să selecteze mai mult de o imagine",
|
||||
"and_launch_surveys_in_your_website_or_app": "și lansați chestionare pe site-ul sau în aplicația dvs.",
|
||||
"animation": "Animație",
|
||||
"any_is_true": "oricare este adevărată",
|
||||
"app_survey_description": "Incorporați un chestionar în aplicația web sau pe site-ul dvs. pentru a colecta răspunsuri.",
|
||||
"assign": "Atribuire =",
|
||||
"audience": "Public",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Urmărirea a fost actualizată și va fi salvată odată ce salvați sondajul.",
|
||||
"follow_ups_new": "Follow-up nou",
|
||||
"follow_ups_upgrade_button_text": "Actualizați pentru a activa urmărările",
|
||||
"form_styling": "Stilizare formular",
|
||||
"formbricks_sdk_is_not_connected": "SDK Formbricks nu este conectat",
|
||||
"four_points": "4 puncte",
|
||||
"heading": "Titlu",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Opțiunea {choiceIndex}",
|
||||
"option_used_in_logic_error": "Această opțiune este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"optional": "Opțional",
|
||||
"options": "Opțiuni",
|
||||
"options": "Opțiuni*",
|
||||
"options_used_in_logic_bulk_error": "Următoarele opțiuni sunt folosite în logică: {questionIndexes}. Vă rugăm să le eliminați din logică mai întâi.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Suprascrie tema cu stiluri individuale pentru acest sondaj.",
|
||||
"overwrite_global_waiting_time": "Setează perioadă de răcire personalizată",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Întrebare ștearsă.",
|
||||
"question_duplicated": "Întrebare duplicată.",
|
||||
"question_id_updated": "ID întrebare actualizat",
|
||||
"question_number": "Întrebarea {number}",
|
||||
"question_used_in_logic_warning_text": "Elemente din acest bloc sunt folosite într-o regulă de logică. Sigur doriți să îl ștergeți?",
|
||||
"question_used_in_logic_warning_title": "Inconsistență logică",
|
||||
"question_used_in_quota": "Întrebarea aceasta este folosită în cota „{quotaName}”",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Limite de răspunsuri, redirecționări și altele.",
|
||||
"response_options": "Opțiuni răspuns",
|
||||
"roundness": "Rotunjire",
|
||||
"roundness_description": "Controlează cât de rotunjite sunt colțurile cardului.",
|
||||
"roundness_description": "Controlează cât de rotunjite sunt colțurile.",
|
||||
"row_used_in_logic_error": "Această linie este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"rows": "Rânduri",
|
||||
"save_and_close": "Salvează & Închide",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Acest sondaj gratuit și open-source a fost închis",
|
||||
"survey_display_settings": "Setări de afișare a sondajului",
|
||||
"survey_placement": "Amplasarea sondajului",
|
||||
"survey_styling": "Stilizare formular",
|
||||
"survey_trigger": "Declanșator sondaj",
|
||||
"switch_multi_language_on_to_get_started": "Activați opțiunea multi-limbă pentru a începe 👉",
|
||||
"target_block_not_found": "Blocul țintă nu a fost găsit",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Perioadă de răcire (între sondaje)",
|
||||
"waiting_time_across_surveys_description": "Pentru a preveni oboseala cauzată de sondaje, alege cum interacționează acest sondaj cu perioada de răcire la nivel de workspace.",
|
||||
"welcome_message": "Mesaj de bun venit",
|
||||
"when": "Când",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Fără un filtru, toți utilizatorii pot fi chestionați.",
|
||||
"you_have_not_created_a_segment_yet": "Nu ai creat încă un segment",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Trebuie să aveți cel puțin două limbi configurate în spațiul de lucru pentru a putea lucra cu traduceri.",
|
||||
"your_description_here_recall_information_with": "Descrierea ta aici. Reamintiți informațiile cu @",
|
||||
"your_question_here_recall_information_with": "Întrebarea ta aici. Reamintiți informațiile cu @",
|
||||
"your_web_app": "Aplicația dumneavoastră web",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Începuturi",
|
||||
"starts_tooltip": "Număr de ori când sondajul a fost început.",
|
||||
"survey_reset_successfully": "Resetarea chestionarului realizată cu succes! Au fost șterse {responseCount} răspunsuri și {displayCount} afișări.",
|
||||
"survey_results": "Rezultatele {surveyName}",
|
||||
"this_month": "Luna aceasta",
|
||||
"this_quarter": "Trimestrul acesta",
|
||||
"this_year": "Anul acesta",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Colorează textul introdus în câmpuri.",
|
||||
"advanced_styling_field_option_bg": "Fundal",
|
||||
"advanced_styling_field_option_bg_description": "Umple elementele de opțiune.",
|
||||
"advanced_styling_field_option_border": "Culoare contur",
|
||||
"advanced_styling_field_option_border_description": "Evidențiază opțiunile radio și checkbox.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rotunjește colțurile opțiunilor.",
|
||||
"advanced_styling_field_option_font_size_description": "Redimensionează textul etichetei opțiunii.",
|
||||
"advanced_styling_field_option_label": "Culoare etichetă",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Cum poate îmbunătăți compania alinierea viziunii și strategiei sale?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Tastează răspunsul aici...",
|
||||
"back": "Înapoi",
|
||||
"block_1": "Blocul 1",
|
||||
"block_10": "Blocul 10",
|
||||
"block_2": "Blocul 2",
|
||||
"block_3": "Blocul 3",
|
||||
"block_4": "Blocul 4",
|
||||
"block_5": "Blocul 5",
|
||||
"block_6": "Blocul 6",
|
||||
"block_7": "Blocul 7",
|
||||
"block_8": "Blocul 8",
|
||||
"block_9": "Blocul 9",
|
||||
"book_interview": "Rezervă interviu",
|
||||
"build_product_roadmap_description": "Identificați acel UN lucru pe care îl doresc cel mai mult utilizatorii și construiți-l.",
|
||||
"build_product_roadmap_name": "Crearea foii de parcurs a produsului",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Of, îmi pare rău! Există ceva ce putem face pentru a-ți îmbunătăți experiența?",
|
||||
"csat_survey_question_3_placeholder": "Tastează răspunsul aici...",
|
||||
"cta_description": "Afișează informații și solicită utilizatorilor să ia o acțiune specifică",
|
||||
"custom_survey_block_1_name": "Bloc 1",
|
||||
"custom_survey_description": "Creează un sondaj fără șablon.",
|
||||
"custom_survey_name": "Începe de la zero",
|
||||
"custom_survey_question_1_headline": "Ce ați dori să știți?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nu, mulţumesc!",
|
||||
"preview_survey_question_2_headline": "Vrei să fii în temă?",
|
||||
"preview_survey_question_2_subheader": "Aceasta este o descriere exemplu.",
|
||||
"preview_survey_question_open_text_headline": "Mai vrei să împărtășești ceva?",
|
||||
"preview_survey_question_open_text_placeholder": "Tastează răspunsul aici...",
|
||||
"preview_survey_question_open_text_subheader": "Feedbackul tău ne ajută să ne îmbunătățim.",
|
||||
"preview_survey_welcome_card_headline": "Bun venit!",
|
||||
"prioritize_features_description": "Identificați caracteristicile de care utilizatorii dumneavoastră au cel mai mult și cel mai puțin nevoie.",
|
||||
"prioritize_features_name": "Prioritizați caracteristicile",
|
||||
|
||||
+39
-14
@@ -112,7 +112,6 @@
|
||||
"link_expired_description": "Ссылка, которой вы воспользовались, больше не действительна."
|
||||
},
|
||||
"common": {
|
||||
"Filter": "Фильтр",
|
||||
"accepted": "Принято",
|
||||
"account": "Аккаунт",
|
||||
"account_settings": "Настройки аккаунта",
|
||||
@@ -176,9 +175,12 @@
|
||||
"copy": "Копировать",
|
||||
"copy_code": "Скопировать код",
|
||||
"copy_link": "Скопировать ссылку",
|
||||
"count_attributes": "{value, plural, one {{value} атрибут} few {{value} атрибута} many {{value} атрибутов} other {{value} атрибута}}",
|
||||
"count_contacts": "{value, plural, one {{value} контакт} few {{value} контакта} many {{value} контактов} other {{value} контактов}}",
|
||||
"count_responses": "{value, plural, one {{value} ответ} few {{value} ответа} many {{value} ответов} other {{value} ответов}}",
|
||||
"count_attributes": "{count, plural, one {{count} атрибут} few {{count} атрибута} many {{count} атрибутов} other {{count} атрибута}}",
|
||||
"count_contacts": "{count, plural, one {{count} контакт} few {{count} контакта} many {{count} контактов} other {{count} контакта}}",
|
||||
"count_members": "{count, plural, one {{count} участник} few {{count} участника} many {{count} участников} other {{count} участника}}",
|
||||
"count_questions": "{count, plural, one {{count} вопрос} few {{count} вопроса} many {{count} вопросов} other {{count} вопросов}}",
|
||||
"count_responses": "{count, plural, one {{count} ответ} few {{count} ответа} many {{count} ответов} other {{count} ответов}}",
|
||||
"count_selections": "{count, plural, one {{count} выбран} few {{count} выбрано} many {{count} выбрано} other {{count} выбрано}}",
|
||||
"create_new_organization": "Создать новую организацию",
|
||||
"create_segment": "Создать сегмент",
|
||||
"create_survey": "Создать опрос",
|
||||
@@ -192,6 +194,7 @@
|
||||
"days": "дни",
|
||||
"default": "По умолчанию",
|
||||
"delete": "Удалить",
|
||||
"delete_what": "Удалить {deleteWhat}",
|
||||
"description": "Описание",
|
||||
"dev_env": "Dev Environment",
|
||||
"development": "Разработка",
|
||||
@@ -207,6 +210,8 @@
|
||||
"download": "Скачать",
|
||||
"draft": "Черновик",
|
||||
"duplicate": "Дублировать",
|
||||
"duplicate_copy": "(копия)",
|
||||
"duplicate_copy_number": "(копия {copyNumber})",
|
||||
"e_commerce": "E-Commerce",
|
||||
"edit": "Редактировать",
|
||||
"email": "Email",
|
||||
@@ -228,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Не удалось загрузить рабочие пространства",
|
||||
"filter": "Фильтр",
|
||||
"finish": "Завершить",
|
||||
"first_name": "Имя",
|
||||
"follow_these": "Выполните следующие действия",
|
||||
"formbricks_version": "Версия Formbricks",
|
||||
"full_name": "Полное имя",
|
||||
@@ -240,6 +246,7 @@
|
||||
"hidden_field": "Скрытое поле",
|
||||
"hidden_fields": "Скрытые поля",
|
||||
"hide_column": "Скрыть столбец",
|
||||
"id": "ID",
|
||||
"image": "Изображение",
|
||||
"images": "Изображения",
|
||||
"import": "Импорт",
|
||||
@@ -257,6 +264,7 @@
|
||||
"key": "Ключ",
|
||||
"label": "Метка",
|
||||
"language": "Язык",
|
||||
"last_name": "Фамилия",
|
||||
"learn_more": "Подробнее",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Светлый оверлей",
|
||||
@@ -271,7 +279,6 @@
|
||||
"look_and_feel": "Внешний вид",
|
||||
"manage": "Управление",
|
||||
"marketing": "Маркетинг",
|
||||
"member": "Участник",
|
||||
"members": "Участники",
|
||||
"members_and_teams": "Участники и команды",
|
||||
"membership_not_found": "Участие не найдено",
|
||||
@@ -283,6 +290,7 @@
|
||||
"move_down": "Переместить вниз",
|
||||
"move_up": "Переместить вверх",
|
||||
"multiple_languages": "Несколько языков",
|
||||
"my_product": "мой продукт",
|
||||
"name": "Имя",
|
||||
"new": "Новый",
|
||||
"new_version_available": "Formbricks {version} уже здесь. Обновитесь сейчас!",
|
||||
@@ -378,8 +386,6 @@
|
||||
"select_teams": "Выбрать команды",
|
||||
"selected": "Выбрано",
|
||||
"selected_questions": "Выбранные вопросы",
|
||||
"selection": "Выбор",
|
||||
"selections": "Выборы",
|
||||
"send_test_email": "Отправить тестовое письмо",
|
||||
"session_not_found": "Сессия не найдена",
|
||||
"settings": "Настройки",
|
||||
@@ -1079,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "Привет, {userName}",
|
||||
"email_customization_preview_email_text": "Это предварительный просмотр письма, чтобы показать, какой логотип будет отображаться в письмах.",
|
||||
"error_deleting_organization_please_try_again": "Ошибка при удалении организации. Пожалуйста, попробуйте ещё раз.",
|
||||
"from_your_organization": "из вашей организации",
|
||||
"from_your_organization": "{memberName} из вашей организации",
|
||||
"invitation_sent_once_more": "Приглашение отправлено ещё раз.",
|
||||
"invite_deleted_successfully": "Приглашение успешно удалено",
|
||||
"invite_expires_on": "Приглашение истекает {date}",
|
||||
@@ -1244,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Добавить плейсхолдер, который будет показан, если нет значения для отображения.",
|
||||
"add_hidden_field_id": "Добавить скрытый ID поля",
|
||||
"add_highlight_border": "Добавить выделяющую рамку",
|
||||
"add_highlight_border_description": "Применяется только к опросам внутри продукта.",
|
||||
"add_logic": "Добавить логику",
|
||||
"add_none_of_the_above": "Добавить вариант «Ничего из вышеперечисленного»",
|
||||
"add_option": "Добавить вариант",
|
||||
@@ -1260,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Изменить сообщение «Опрос закрыт»",
|
||||
"adjust_survey_closed_message_description": "Измените сообщение, которое видят посетители, когда опрос закрыт.",
|
||||
"adjust_the_theme_in_the": "Настройте тему в",
|
||||
"all_are_true": "все условия выполняются",
|
||||
"all_other_answers_will_continue_to": "Все остальные ответы будут продолжать",
|
||||
"allow_multi_select": "Разрешить множественный выбор",
|
||||
"allow_multiple_files": "Разрешить несколько файлов",
|
||||
"allow_users_to_select_more_than_one_image": "Разрешить пользователям выбирать более одного изображения",
|
||||
"and_launch_surveys_in_your_website_or_app": "и запускать опросы на вашем сайте или в приложении.",
|
||||
"animation": "Анимация",
|
||||
"any_is_true": "выполняется хотя бы одно условие",
|
||||
"app_survey_description": "Встраивайте опрос в ваше веб-приложение или сайт для сбора ответов.",
|
||||
"assign": "Назначить =",
|
||||
"audience": "Аудитория",
|
||||
@@ -1442,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Фоллоу-ап обновлён и будет сохранён после сохранения опроса.",
|
||||
"follow_ups_new": "Новый фоллоу-ап",
|
||||
"follow_ups_upgrade_button_text": "Обновите тариф для активации фоллоу-апов",
|
||||
"form_styling": "Оформление формы",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDK не подключён",
|
||||
"four_points": "4 балла",
|
||||
"heading": "Заголовок",
|
||||
@@ -1532,7 +1540,7 @@
|
||||
"option_idx": "Вариант {choiceIndex}",
|
||||
"option_used_in_logic_error": "Этот вариант используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
|
||||
"optional": "Необязательно",
|
||||
"options": "Варианты",
|
||||
"options": "Варианты*",
|
||||
"options_used_in_logic_bulk_error": "Следующие варианты используются в логике: {questionIndexes}. Пожалуйста, сначала удалите их из логики.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Переопределить тему индивидуальными стилями для этого опроса.",
|
||||
"overwrite_global_waiting_time": "Установить свой период ожидания",
|
||||
@@ -1557,6 +1565,7 @@
|
||||
"question_deleted": "Вопрос удалён.",
|
||||
"question_duplicated": "Вопрос дублирован.",
|
||||
"question_id_updated": "ID вопроса обновлён",
|
||||
"question_number": "Вопрос {number}",
|
||||
"question_used_in_logic_warning_text": "Элементы из этого блока используются в правиле логики. Вы уверены, что хотите удалить его?",
|
||||
"question_used_in_logic_warning_title": "Несогласованность логики",
|
||||
"question_used_in_quota": "Этот вопрос используется в квоте «{quotaName}»",
|
||||
@@ -1615,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Лимиты ответов, перенаправления и другое.",
|
||||
"response_options": "Параметры ответа",
|
||||
"roundness": "Скругление",
|
||||
"roundness_description": "Определяет степень скругления углов карточки.",
|
||||
"roundness_description": "Определяет степень скругления углов.",
|
||||
"row_used_in_logic_error": "Эта строка используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите её из логики.",
|
||||
"rows": "Строки",
|
||||
"save_and_close": "Сохранить и закрыть",
|
||||
@@ -1661,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Этот бесплатный и открытый опрос был закрыт",
|
||||
"survey_display_settings": "Настройки отображения опроса",
|
||||
"survey_placement": "Размещение опроса",
|
||||
"survey_styling": "Оформление формы",
|
||||
"survey_trigger": "Триггер опроса",
|
||||
"switch_multi_language_on_to_get_started": "Включите многоязычный режим, чтобы начать 👉",
|
||||
"target_block_not_found": "Целевой блок не найден",
|
||||
@@ -1749,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Период ожидания (между опросами)",
|
||||
"waiting_time_across_surveys_description": "Чтобы избежать усталости от опросов, выберите, как этот опрос взаимодействует с общим периодом ожидания в рабочем пространстве.",
|
||||
"welcome_message": "Приветственное сообщение",
|
||||
"when": "Когда",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Без фильтра все ваши пользователи могут быть опрошены.",
|
||||
"you_have_not_created_a_segment_yet": "Вы ещё не создали сегмент",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Для работы с переводами необходимо настроить два или более языков в рабочем пространстве.",
|
||||
"your_description_here_recall_information_with": "Ваша инструкция здесь. Вспомните информацию с помощью @",
|
||||
"your_question_here_recall_information_with": "Ваш вопрос здесь. Вспомните информацию с помощью @",
|
||||
"your_web_app": "Ваше веб-приложение",
|
||||
@@ -2025,6 +2035,7 @@
|
||||
"starts": "Запуски",
|
||||
"starts_tooltip": "Количество запусков опроса.",
|
||||
"survey_reset_successfully": "Опрос успешно сброшен! {responseCount} ответов и {displayCount} показов были удалены.",
|
||||
"survey_results": "Результаты {surveyName}",
|
||||
"this_month": "В этом месяце",
|
||||
"this_quarter": "В этом квартале",
|
||||
"this_year": "В этом году",
|
||||
@@ -2172,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Задаёт цвет заполненной части полосы.",
|
||||
"advanced_styling_field_input_border_radius_description": "Скругляет углы полей ввода.",
|
||||
"advanced_styling_field_input_font_size_description": "Масштабирует введённый текст в полях ввода.",
|
||||
"advanced_styling_field_input_height_description": "Определяет минимальную высоту поля ввода.",
|
||||
"advanced_styling_field_input_height_description": "Управляет минимальной высотой поля ввода.",
|
||||
"advanced_styling_field_input_padding_x_description": "Добавляет отступы слева и справа.",
|
||||
"advanced_styling_field_input_padding_y_description": "Добавляет пространство сверху и снизу.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Делает текст подсказки менее заметным.",
|
||||
@@ -2181,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Задаёт цвет введённого текста в полях.",
|
||||
"advanced_styling_field_option_bg": "Фон",
|
||||
"advanced_styling_field_option_bg_description": "Заливает фон элементов опций.",
|
||||
"advanced_styling_field_option_border": "Цвет границы",
|
||||
"advanced_styling_field_option_border_description": "Обводка для вариантов radio и checkbox.",
|
||||
"advanced_styling_field_option_border_radius_description": "Скругляет углы опций.",
|
||||
"advanced_styling_field_option_font_size_description": "Изменяет размер текста метки опции.",
|
||||
"advanced_styling_field_option_label": "Цвет метки",
|
||||
@@ -2390,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Как компания может улучшить согласованность видения и стратегии?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Введите ваш ответ здесь...",
|
||||
"back": "Назад",
|
||||
"block_1": "Блок 1",
|
||||
"block_10": "Блок 10",
|
||||
"block_2": "Блок 2",
|
||||
"block_3": "Блок 3",
|
||||
"block_4": "Блок 4",
|
||||
"block_5": "Блок 5",
|
||||
"block_6": "Блок 6",
|
||||
"block_7": "Блок 7",
|
||||
"block_8": "Блок 8",
|
||||
"block_9": "Блок 9",
|
||||
"book_interview": "Записаться на интервью",
|
||||
"build_product_roadmap_description": "Определите ОДНУ вещь, которую ваши пользователи хотят больше всего, и реализуйте её.",
|
||||
"build_product_roadmap_name": "Построение продуктовой дорожной карты",
|
||||
@@ -2597,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ой, извините! Что мы можем сделать, чтобы улучшить ваш опыт?",
|
||||
"csat_survey_question_3_placeholder": "Введите ваш ответ здесь...",
|
||||
"cta_description": "Показывайте информацию и побуждайте пользователей к определённому действию",
|
||||
"custom_survey_block_1_name": "Блок 1",
|
||||
"custom_survey_description": "Создайте опрос без шаблона.",
|
||||
"custom_survey_name": "Начать с нуля",
|
||||
"custom_survey_question_1_headline": "Что вы хотели бы узнать?",
|
||||
@@ -3000,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Нет, спасибо!",
|
||||
"preview_survey_question_2_headline": "Хотите быть в курсе событий?",
|
||||
"preview_survey_question_2_subheader": "Это пример описания.",
|
||||
"preview_survey_question_open_text_headline": "Есть ли ещё что-то, чем хочешь поделиться?",
|
||||
"preview_survey_question_open_text_placeholder": "Введи свой ответ здесь...",
|
||||
"preview_survey_question_open_text_subheader": "Твой отзыв помогает нам становиться лучше.",
|
||||
"preview_survey_welcome_card_headline": "Добро пожаловать!",
|
||||
"prioritize_features_description": "Определите, какие функции наиболее и наименее важны для ваших пользователей.",
|
||||
"prioritize_features_name": "Приоритизация функций",
|
||||
|
||||
+38
-12
@@ -175,9 +175,12 @@
|
||||
"copy": "Kopiera",
|
||||
"copy_code": "Kopiera kod",
|
||||
"copy_link": "Kopiera länk",
|
||||
"count_attributes": "{value, plural, one {{value} attribut} other {{value} attribut}}",
|
||||
"count_contacts": "{value, plural, one {{value} kontakt} other {{value} kontakter}}",
|
||||
"count_responses": "{value, plural, one {{value} svar} other {{value} svar}}",
|
||||
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attribut}}",
|
||||
"count_contacts": "{count, plural, one {{count} kontakt} other {{count} kontakter}}",
|
||||
"count_members": "{count, plural, one {{count} medlem} other {{count} medlemmar}}",
|
||||
"count_questions": "{count, plural, one {{count} fråga} other {{count} frågor}}",
|
||||
"count_responses": "{count, plural, one {{count} svar} other {{count} svar}}",
|
||||
"count_selections": "{count, plural, one {{count} val} other {{count} val}}",
|
||||
"create_new_organization": "Skapa ny organisation",
|
||||
"create_segment": "Skapa segment",
|
||||
"create_survey": "Skapa enkät",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "dagar",
|
||||
"default": "Standard",
|
||||
"delete": "Ta bort",
|
||||
"delete_what": "Ta bort {deleteWhat}",
|
||||
"description": "Beskrivning",
|
||||
"dev_env": "Utvecklingsmiljö",
|
||||
"development": "Utveckling",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "Ladda ner",
|
||||
"draft": "Utkast",
|
||||
"duplicate": "Duplicera",
|
||||
"duplicate_copy": "(kopia)",
|
||||
"duplicate_copy_number": "(kopia {copyNumber})",
|
||||
"e_commerce": "E-handel",
|
||||
"edit": "Redigera",
|
||||
"email": "E-post",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "Det gick inte att ladda arbetsytor",
|
||||
"filter": "Filter",
|
||||
"finish": "Slutför",
|
||||
"first_name": "Förnamn",
|
||||
"follow_these": "Följ dessa",
|
||||
"formbricks_version": "Formbricks-version",
|
||||
"full_name": "Fullständigt namn",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "Dolt fält",
|
||||
"hidden_fields": "Dolda fält",
|
||||
"hide_column": "Dölj kolumn",
|
||||
"id": "ID",
|
||||
"image": "Bild",
|
||||
"images": "Bilder",
|
||||
"import": "Importera",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "Nyckel",
|
||||
"label": "Etikett",
|
||||
"language": "Språk",
|
||||
"last_name": "Efternamn",
|
||||
"learn_more": "Läs mer",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "Ljust överlägg",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "Utseende",
|
||||
"manage": "Hantera",
|
||||
"marketing": "Marknadsföring",
|
||||
"member": "Medlem",
|
||||
"members": "Medlemmar",
|
||||
"members_and_teams": "Medlemmar och team",
|
||||
"membership_not_found": "Medlemskap hittades inte",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "Flytta ner",
|
||||
"move_up": "Flytta upp",
|
||||
"multiple_languages": "Flera språk",
|
||||
"my_product": "min produkt",
|
||||
"name": "Namn",
|
||||
"new": "Ny",
|
||||
"new_version_available": "Formbricks {version} är här. Uppgradera nu!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "Välj team",
|
||||
"selected": "Vald",
|
||||
"selected_questions": "Valda frågor",
|
||||
"selection": "Urval",
|
||||
"selections": "Urval",
|
||||
"send_test_email": "Skicka testmeddelande",
|
||||
"session_not_found": "Session hittades inte",
|
||||
"settings": "Inställningar",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "Lägg till en platshållare att visa om det inte finns något värde att återkalla.",
|
||||
"add_hidden_field_id": "Lägg till dolt fält-ID",
|
||||
"add_highlight_border": "Lägg till markerad kant",
|
||||
"add_highlight_border_description": "Gäller bara för undersökningar i produkten.",
|
||||
"add_logic": "Lägg till logik",
|
||||
"add_none_of_the_above": "Lägg till \"Inget av ovanstående\"",
|
||||
"add_option": "Lägg till alternativ",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Justera meddelande för 'Enkät stängd'",
|
||||
"adjust_survey_closed_message_description": "Ändra meddelandet besökare ser när enkäten är stängd.",
|
||||
"adjust_the_theme_in_the": "Justera temat i",
|
||||
"all_are_true": "alla är sanna",
|
||||
"all_other_answers_will_continue_to": "Alla andra svar fortsätter till",
|
||||
"allow_multi_select": "Tillåt flerval",
|
||||
"allow_multiple_files": "Tillåt flera filer",
|
||||
"allow_users_to_select_more_than_one_image": "Tillåt användare att välja mer än en bild",
|
||||
"and_launch_surveys_in_your_website_or_app": "och starta enkäter på din webbplats eller i din app.",
|
||||
"animation": "Animering",
|
||||
"any_is_true": "någon är sann",
|
||||
"app_survey_description": "Bädda in en enkät i din webbapp eller webbplats för att samla in svar.",
|
||||
"assign": "Tilldela =",
|
||||
"audience": "Målgrupp",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "Uppföljning uppdaterad och sparas när du sparar enkäten.",
|
||||
"follow_ups_new": "Ny uppföljning",
|
||||
"follow_ups_upgrade_button_text": "Uppgradera för att aktivera uppföljningar",
|
||||
"form_styling": "Formulärstil",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDK är inte anslutet",
|
||||
"four_points": "4 poäng",
|
||||
"heading": "Rubrik",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "Alternativ {choiceIndex}",
|
||||
"option_used_in_logic_error": "Detta alternativ används i logiken för fråga {questionIndex}. Vänligen ta bort det från logiken först.",
|
||||
"optional": "Valfritt",
|
||||
"options": "Alternativ",
|
||||
"options": "Alternativ*",
|
||||
"options_used_in_logic_bulk_error": "Följande alternativ används i logiken: {questionIndexes}. Vänligen ta bort dem från logiken först.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Åsidosätt temat med individuella stilar för denna enkät.",
|
||||
"overwrite_global_waiting_time": "Ange anpassad väntetid",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "Fråga borttagen.",
|
||||
"question_duplicated": "Fråga duplicerad.",
|
||||
"question_id_updated": "Fråge-ID uppdaterat",
|
||||
"question_number": "Fråga {number}",
|
||||
"question_used_in_logic_warning_text": "Element från det här blocket används i en logikregel. Är du säker på att du vill ta bort det?",
|
||||
"question_used_in_logic_warning_title": "Logikkonflikt",
|
||||
"question_used_in_quota": "Denna fråga används i kvoten “{quotaName}”",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "Svarsgränser, omdirigeringar och mer.",
|
||||
"response_options": "Svarsalternativ",
|
||||
"roundness": "Rundhet",
|
||||
"roundness_description": "Styr hur rundade kortets hörn är.",
|
||||
"roundness_description": "Styr hur rundade hörnen är.",
|
||||
"row_used_in_logic_error": "Denna rad används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
|
||||
"rows": "Rader",
|
||||
"save_and_close": "Spara och stäng",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "Denna gratis och öppenkällkodsenkät har stängts",
|
||||
"survey_display_settings": "Visningsinställningar för enkät",
|
||||
"survey_placement": "Enkätplacering",
|
||||
"survey_styling": "Formulärstil",
|
||||
"survey_trigger": "Enkätutlösare",
|
||||
"switch_multi_language_on_to_get_started": "Slå på flerspråkighet för att komma igång 👉",
|
||||
"target_block_not_found": "Målblock hittades inte",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "Väntetid (mellan enkäter)",
|
||||
"waiting_time_across_surveys_description": "För att undvika enkättrötthet, välj hur denna enkät ska förhålla sig till arbetsytans gemensamma väntetid.",
|
||||
"welcome_message": "Välkomstmeddelande",
|
||||
"when": "När",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Utan ett filter kan alla dina användare enkäteras.",
|
||||
"you_have_not_created_a_segment_yet": "Du har inte skapat ett segment ännu",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Du måste ha två eller fler språk inställda i din arbetsyta för att kunna arbeta med översättningar.",
|
||||
"your_description_here_recall_information_with": "Din beskrivning här. Återkalla information med @",
|
||||
"your_question_here_recall_information_with": "Din fråga här. Återkalla information med @",
|
||||
"your_web_app": "Din webbapp",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "Starter",
|
||||
"starts_tooltip": "Antal gånger enkäten har startats.",
|
||||
"survey_reset_successfully": "Enkät återställd! {responseCount} svar och {displayCount} visningar togs bort.",
|
||||
"survey_results": "Resultat för {surveyName}",
|
||||
"this_month": "Denna månad",
|
||||
"this_quarter": "Detta kvartal",
|
||||
"this_year": "Detta år",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "Färglägger den fyllda delen av stapeln.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rundar av hörnen på inmatningsfält.",
|
||||
"advanced_styling_field_input_font_size_description": "Ändrar storleken på texten i inmatningsfält.",
|
||||
"advanced_styling_field_input_height_description": "Styr den minsta höjden på inmatningsfältet.",
|
||||
"advanced_styling_field_input_height_description": "Styr minsta höjden på inmatningsfältet.",
|
||||
"advanced_styling_field_input_padding_x_description": "Lägger till utrymme till vänster och höger.",
|
||||
"advanced_styling_field_input_padding_y_description": "Lägger till utrymme upptill och nedtill.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Tonar ut platshållartexten.",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "Färgar den inmatade texten i fälten.",
|
||||
"advanced_styling_field_option_bg": "Bakgrund",
|
||||
"advanced_styling_field_option_bg_description": "Fyller alternativraderna.",
|
||||
"advanced_styling_field_option_border": "Kantfärg",
|
||||
"advanced_styling_field_option_border_description": "Markerar radio- och kryssrutealternativ.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rundar hörnen på alternativen.",
|
||||
"advanced_styling_field_option_font_size_description": "Skalar textstorleken på alternativetiketten.",
|
||||
"advanced_styling_field_option_label": "Etikettfärg",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Hur kan företaget förbättra sin vision och strategiöverensstämmelse?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Skriv ditt svar här...",
|
||||
"back": "Tillbaka",
|
||||
"block_1": "Block 1",
|
||||
"block_10": "Block 10",
|
||||
"block_2": "Block 2",
|
||||
"block_3": "Block 3",
|
||||
"block_4": "Block 4",
|
||||
"block_5": "Block 5",
|
||||
"block_6": "Block 6",
|
||||
"block_7": "Block 7",
|
||||
"block_8": "Block 8",
|
||||
"block_9": "Block 9",
|
||||
"book_interview": "Boka intervju",
|
||||
"build_product_roadmap_description": "Identifiera det EN sak dina användare vill ha mest och bygg den.",
|
||||
"build_product_roadmap_name": "Bygg produktroadmap",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Aj, förlåt! Finns det något vi kan göra för att förbättra din upplevelse?",
|
||||
"csat_survey_question_3_placeholder": "Skriv ditt svar här...",
|
||||
"cta_description": "Visa information och uppmana användare att vidta en specifik åtgärd",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Skapa en enkät utan mall.",
|
||||
"custom_survey_name": "Börja från början",
|
||||
"custom_survey_question_1_headline": "Vad vill du veta?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nej, tack!",
|
||||
"preview_survey_question_2_headline": "Vill du hållas uppdaterad?",
|
||||
"preview_survey_question_2_subheader": "Det här är ett exempel på en beskrivning.",
|
||||
"preview_survey_question_open_text_headline": "Något mer du vill dela med dig av?",
|
||||
"preview_survey_question_open_text_placeholder": "Skriv ditt svar här...",
|
||||
"preview_survey_question_open_text_subheader": "Din feedback hjälper oss att bli bättre.",
|
||||
"preview_survey_welcome_card_headline": "Välkommen!",
|
||||
"prioritize_features_description": "Identifiera vilka funktioner dina användare behöver mest och minst.",
|
||||
"prioritize_features_name": "Prioritera funktioner",
|
||||
|
||||
@@ -175,9 +175,12 @@
|
||||
"copy": "复制",
|
||||
"copy_code": "复制 代码",
|
||||
"copy_link": "复制 链接",
|
||||
"count_attributes": "{value, plural, one {{value} 个属性} other {{value} 个属性}}",
|
||||
"count_contacts": "{value, plural, other {{value} 联系人} }",
|
||||
"count_responses": "{value, plural, other {{value} 回复} }",
|
||||
"count_attributes": "{count, plural, other {{count} 个属性}}",
|
||||
"count_contacts": "{count, plural, other {{count} 个联系人}}",
|
||||
"count_members": "{count, plural, one {{count} 位成员} other {{count} 位成员}}",
|
||||
"count_questions": "{count, plural, other {{count} 个问题} }",
|
||||
"count_responses": "{count, plural, other {{count} 回复} }",
|
||||
"count_selections": "{count, plural, other {已选择{count}项}}",
|
||||
"create_new_organization": "创建 新的 组织",
|
||||
"create_segment": "创建 细分",
|
||||
"create_survey": "创建 调查",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "天",
|
||||
"default": "默认",
|
||||
"delete": "删除",
|
||||
"delete_what": "删除{deleteWhat}",
|
||||
"description": "描述",
|
||||
"dev_env": "开发 环境",
|
||||
"development": "开发环境",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "下载",
|
||||
"draft": "草稿",
|
||||
"duplicate": "复制",
|
||||
"duplicate_copy": "(副本)",
|
||||
"duplicate_copy_number": "(副本 {copyNumber})",
|
||||
"e_commerce": "电子商务",
|
||||
"edit": "编辑",
|
||||
"email": "邮箱",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "加载工作区失败",
|
||||
"filter": "筛选",
|
||||
"finish": "完成",
|
||||
"first_name": "名字",
|
||||
"follow_these": "遵循 这些",
|
||||
"formbricks_version": "Formbricks 版本",
|
||||
"full_name": "全名",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "隐藏 字段",
|
||||
"hidden_fields": "隐藏 字段",
|
||||
"hide_column": "隐藏 列",
|
||||
"id": "ID",
|
||||
"image": "图片",
|
||||
"images": "图片",
|
||||
"import": "导入",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "键",
|
||||
"label": "标签",
|
||||
"language": "语言",
|
||||
"last_name": "姓",
|
||||
"learn_more": "了解 更多",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "浅色遮罩层",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "外观 & 感觉",
|
||||
"manage": "管理",
|
||||
"marketing": "市场营销",
|
||||
"member": "成员",
|
||||
"members": "成员",
|
||||
"members_and_teams": "成员和团队",
|
||||
"membership_not_found": "未找到会员资格",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "下移",
|
||||
"move_up": "上移",
|
||||
"multiple_languages": "多种 语言",
|
||||
"my_product": "我的产品",
|
||||
"name": "名称",
|
||||
"new": "新建",
|
||||
"new_version_available": "Formbricks {version} 在 这里。立即 升级!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "选择 团队",
|
||||
"selected": "已选择",
|
||||
"selected_questions": "选择的问题",
|
||||
"selection": "选择",
|
||||
"selections": "选择",
|
||||
"send_test_email": "发送 测试 电子邮件",
|
||||
"session_not_found": "会话 未找到",
|
||||
"settings": "设置",
|
||||
@@ -1078,7 +1085,7 @@
|
||||
"email_customization_preview_email_heading": "嘿 {userName}",
|
||||
"email_customization_preview_email_text": "这 是 一封 电子邮件 预览,展示 哪个 徽标 将在 电子邮件 中 渲染。",
|
||||
"error_deleting_organization_please_try_again": "删除 组织时 出错 。 请重试 。",
|
||||
"from_your_organization": "来自你的组织",
|
||||
"from_your_organization": "来自您组织的{memberName}",
|
||||
"invitation_sent_once_more": "再次发送邀请。",
|
||||
"invite_deleted_successfully": "邀请 删除 成功",
|
||||
"invite_expires_on": "邀请将于 {date} 过期",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "添加 占位符 显示 如果 没有 值以 回忆",
|
||||
"add_hidden_field_id": "添加 隐藏 字段 ID",
|
||||
"add_highlight_border": "添加 高亮 边框",
|
||||
"add_highlight_border_description": "仅适用于产品内调查。",
|
||||
"add_logic": "添加逻辑",
|
||||
"add_none_of_the_above": "添加 “以上 都 不 是”",
|
||||
"add_option": "添加 选项",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "调整 \"调查 关闭\" 消息",
|
||||
"adjust_survey_closed_message_description": "更改 访客 看到 调查 关闭 时 的 消息。",
|
||||
"adjust_the_theme_in_the": "调整主题在",
|
||||
"all_are_true": "全部为真",
|
||||
"all_other_answers_will_continue_to": "所有其他答案将继续",
|
||||
"allow_multi_select": "允许 多选",
|
||||
"allow_multiple_files": "允许 多 个 文件",
|
||||
"allow_users_to_select_more_than_one_image": "允许 用户 选择 多于 一个 图片",
|
||||
"and_launch_surveys_in_your_website_or_app": "并 在 你 的 网站 或 应用 中 启动 问卷 。",
|
||||
"animation": "动画",
|
||||
"any_is_true": "任一为真",
|
||||
"app_survey_description": "在 你的 网络 应用 或 网站 中 嵌入 问卷 收集 反馈 。",
|
||||
"assign": "指派 =",
|
||||
"audience": "受众",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "后续 操作 已 更新, 并且 在 你 保存 调查 后 将 被 保存。",
|
||||
"follow_ups_new": "新的跟进",
|
||||
"follow_ups_upgrade_button_text": "升级 以启用 跟进",
|
||||
"form_styling": "表单 样式",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDK 未连接",
|
||||
"four_points": "4 分",
|
||||
"heading": "标题",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "选项 {choiceIndex}",
|
||||
"option_used_in_logic_error": "\"这个 选项 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"optional": "可选",
|
||||
"options": "选项",
|
||||
"options": "选项*",
|
||||
"options_used_in_logic_bulk_error": "以下选项在逻辑中被使用:{questionIndexes}。请先从逻辑中删除它们。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用 个性化 样式 替代 这份 问卷 的 主题。",
|
||||
"overwrite_global_waiting_time": "自定义冷却期",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "问题 已删除",
|
||||
"question_duplicated": "问题重复。",
|
||||
"question_id_updated": "问题 ID 更新",
|
||||
"question_number": "第 {number} 题",
|
||||
"question_used_in_logic_warning_text": "此区块中的元素已被用于逻辑规则,您确定要删除吗?",
|
||||
"question_used_in_logic_warning_title": "逻辑不一致",
|
||||
"question_used_in_quota": "此问题正在被“{quotaName}”配额使用",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "响应 限制 、 重定向 和 更多 。",
|
||||
"response_options": "响应 选项",
|
||||
"roundness": "圆度",
|
||||
"roundness_description": "控制卡片角的圆润程度。",
|
||||
"roundness_description": "控制圆角的弧度。",
|
||||
"row_used_in_logic_error": "\"这个 行 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"rows": "行",
|
||||
"save_and_close": "保存 和 关闭",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "此 免费 & 开源 调查 已 关闭",
|
||||
"survey_display_settings": "调查显示设置",
|
||||
"survey_placement": "调查 放置",
|
||||
"survey_styling": "表单 样式",
|
||||
"survey_trigger": "调查 触发",
|
||||
"switch_multi_language_on_to_get_started": "开启多语言以开始使用 👉",
|
||||
"target_block_not_found": "未找到目标区块",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "冷却期(跨问卷)",
|
||||
"waiting_time_across_surveys_description": "为防止问卷疲劳,请选择此问卷与工作区冷却期的交互方式。",
|
||||
"welcome_message": "欢迎 信息",
|
||||
"when": "当",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "没有 过滤器 时 ,所有 用户 都可以 被 调查 。",
|
||||
"you_have_not_created_a_segment_yet": "您 还没有 创建 段落",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "要使用翻译功能,您的工作区需设置两种或以上语言。",
|
||||
"your_description_here_recall_information_with": "在此输入描述。 调用信息与 @",
|
||||
"your_question_here_recall_information_with": "在此输入你的问题。 调用信息与 @",
|
||||
"your_web_app": "您的 网页应用",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "开始",
|
||||
"starts_tooltip": "调查 被 开始 的 次数",
|
||||
"survey_reset_successfully": "调查已重置成功!{responseCount} 个 反馈 和 {displayCount} 个 显示 已删除。",
|
||||
"survey_results": "{surveyName} 结果",
|
||||
"this_month": "本月",
|
||||
"this_quarter": "本季度",
|
||||
"this_year": "今年",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "设置进度条已填充部分的颜色。",
|
||||
"advanced_styling_field_input_border_radius_description": "设置输入框圆角。",
|
||||
"advanced_styling_field_input_font_size_description": "调整输入框内文字大小。",
|
||||
"advanced_styling_field_input_height_description": "设置输入框的最小高度。",
|
||||
"advanced_styling_field_input_height_description": "控制输入框的最小高度。",
|
||||
"advanced_styling_field_input_padding_x_description": "增加输入框左右间距。",
|
||||
"advanced_styling_field_input_padding_y_description": "为输入框上下添加间距。",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "调整占位提示文字的透明度。",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "设置输入框内已输入文字的颜色。",
|
||||
"advanced_styling_field_option_bg": "背景色",
|
||||
"advanced_styling_field_option_bg_description": "设置选项项的背景色。",
|
||||
"advanced_styling_field_option_border": "边框颜色",
|
||||
"advanced_styling_field_option_border_description": "为单选框和复选框选项添加轮廓。",
|
||||
"advanced_styling_field_option_border_radius_description": "设置选项的圆角。",
|
||||
"advanced_styling_field_option_font_size_description": "调整选项标签文字的大小。",
|
||||
"advanced_styling_field_option_label": "标签颜色",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "公司 如何 改进 其 愿景 与 战略 的 协同?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "输入您的答案...",
|
||||
"back": "返回",
|
||||
"block_1": "第 1 块",
|
||||
"block_10": "第 10 块",
|
||||
"block_2": "第 2 块",
|
||||
"block_3": "第 3 块",
|
||||
"block_4": "第 4 块",
|
||||
"block_5": "第 5 块",
|
||||
"block_6": "第 6 块",
|
||||
"block_7": "第 7 块",
|
||||
"block_8": "第 8 块",
|
||||
"block_9": "第 9 块",
|
||||
"book_interview": "预约 面试",
|
||||
"build_product_roadmap_description": "识别 用户 最 想要 的 一个 东西 并 构建 它 。",
|
||||
"build_product_roadmap_name": "构建 产品 路线图",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "糟糕, 对不起!我们可以做些什么来改善您的体验?",
|
||||
"csat_survey_question_3_placeholder": "在此输入您的答案...",
|
||||
"cta_description": "显示 信息 并 提示用户采取 特定行动",
|
||||
"custom_survey_block_1_name": "模块 1",
|
||||
"custom_survey_description": "创建 一个 没有 模板 的 调查。",
|
||||
"custom_survey_name": "从零开始",
|
||||
"custom_survey_question_1_headline": "你 想 知道 什么?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "不,谢谢!",
|
||||
"preview_survey_question_2_headline": "想 了解 最新信息吗?",
|
||||
"preview_survey_question_2_subheader": "这是一个示例描述。",
|
||||
"preview_survey_question_open_text_headline": "还有什么想和我们分享的吗?",
|
||||
"preview_survey_question_open_text_placeholder": "请在这里输入你的答案...",
|
||||
"preview_survey_question_open_text_subheader": "你的反馈能帮助我们改进。",
|
||||
"preview_survey_welcome_card_headline": "欢迎!",
|
||||
"prioritize_features_description": "确定 用户 最 需要 和 最 不 需要 的 功能。",
|
||||
"prioritize_features_name": "优先 功能",
|
||||
|
||||
@@ -175,9 +175,12 @@
|
||||
"copy": "複製",
|
||||
"copy_code": "複製程式碼",
|
||||
"copy_link": "複製連結",
|
||||
"count_attributes": "{value, plural, one {{value} 個屬性} other {{value} 個屬性}}",
|
||||
"count_contacts": "{value, plural, other {{value} 聯絡人} }",
|
||||
"count_responses": "{value, plural, other {{value} 回應} }",
|
||||
"count_attributes": "{count, plural, other {{count} 個屬性}}",
|
||||
"count_contacts": "{count, plural, other {{count} 位聯絡人}}",
|
||||
"count_members": "{count, plural, one {{count} 位成員} other {{count} 位成員}}",
|
||||
"count_questions": "{count, plural, other {{count} 個問題}}",
|
||||
"count_responses": "{count, plural, other {{count} 回應} }",
|
||||
"count_selections": "{count, plural, one {{count} 個選項} other {{count} 個選項}}",
|
||||
"create_new_organization": "建立新組織",
|
||||
"create_segment": "建立區隔",
|
||||
"create_survey": "建立問卷",
|
||||
@@ -191,6 +194,7 @@
|
||||
"days": "天",
|
||||
"default": "預設",
|
||||
"delete": "刪除",
|
||||
"delete_what": "刪除{deleteWhat}",
|
||||
"description": "描述",
|
||||
"dev_env": "開發環境",
|
||||
"development": "開發",
|
||||
@@ -206,6 +210,8 @@
|
||||
"download": "下載",
|
||||
"draft": "草稿",
|
||||
"duplicate": "複製",
|
||||
"duplicate_copy": "(複製)",
|
||||
"duplicate_copy_number": "(複製 {copyNumber})",
|
||||
"e_commerce": "電子商務",
|
||||
"edit": "編輯",
|
||||
"email": "電子郵件",
|
||||
@@ -227,6 +233,7 @@
|
||||
"failed_to_load_workspaces": "載入工作區失敗",
|
||||
"filter": "篩選",
|
||||
"finish": "完成",
|
||||
"first_name": "名字",
|
||||
"follow_these": "按照這些步驟",
|
||||
"formbricks_version": "Formbricks 版本",
|
||||
"full_name": "全名",
|
||||
@@ -239,6 +246,7 @@
|
||||
"hidden_field": "隱藏欄位",
|
||||
"hidden_fields": "隱藏欄位",
|
||||
"hide_column": "隱藏欄位",
|
||||
"id": "ID",
|
||||
"image": "圖片",
|
||||
"images": "圖片",
|
||||
"import": "匯入",
|
||||
@@ -256,6 +264,7 @@
|
||||
"key": "金鑰",
|
||||
"label": "標籤",
|
||||
"language": "語言",
|
||||
"last_name": "姓氏",
|
||||
"learn_more": "瞭解更多",
|
||||
"license_expired": "License Expired",
|
||||
"light_overlay": "淺色覆蓋",
|
||||
@@ -270,7 +279,6 @@
|
||||
"look_and_feel": "外觀與風格",
|
||||
"manage": "管理",
|
||||
"marketing": "行銷",
|
||||
"member": "成員",
|
||||
"members": "成員",
|
||||
"members_and_teams": "成員與團隊",
|
||||
"membership_not_found": "找不到成員資格",
|
||||
@@ -282,6 +290,7 @@
|
||||
"move_down": "下移",
|
||||
"move_up": "上移",
|
||||
"multiple_languages": "多種語言",
|
||||
"my_product": "我的產品",
|
||||
"name": "名稱",
|
||||
"new": "新增",
|
||||
"new_version_available": "Formbricks '{'version'}' 已推出。立即升級!",
|
||||
@@ -377,8 +386,6 @@
|
||||
"select_teams": "選擇 團隊",
|
||||
"selected": "已選取",
|
||||
"selected_questions": "選取的問題",
|
||||
"selection": "選取",
|
||||
"selections": "選取",
|
||||
"send_test_email": "發送測試電子郵件",
|
||||
"session_not_found": "找不到工作階段",
|
||||
"settings": "設定",
|
||||
@@ -1243,6 +1250,7 @@
|
||||
"add_fallback_placeholder": "新增 預設 以顯示是否沒 有 值 可 回憶 。",
|
||||
"add_hidden_field_id": "新增隱藏欄位 ID",
|
||||
"add_highlight_border": "新增醒目提示邊框",
|
||||
"add_highlight_border_description": "僅適用於產品內調查。",
|
||||
"add_logic": "新增邏輯",
|
||||
"add_none_of_the_above": "新增 \"以上皆非\"",
|
||||
"add_option": "新增選項",
|
||||
@@ -1259,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "調整「問卷已關閉」訊息",
|
||||
"adjust_survey_closed_message_description": "變更訪客在問卷關閉時看到的訊息。",
|
||||
"adjust_the_theme_in_the": "在",
|
||||
"all_are_true": "全部為真",
|
||||
"all_other_answers_will_continue_to": "所有其他答案將繼續",
|
||||
"allow_multi_select": "允許多重選取",
|
||||
"allow_multiple_files": "允許上傳多個檔案",
|
||||
"allow_users_to_select_more_than_one_image": "允許使用者選取多張圖片",
|
||||
"and_launch_surveys_in_your_website_or_app": "並在您的網站或應用程式中啟動問卷。",
|
||||
"animation": "動畫",
|
||||
"any_is_true": "任一為真",
|
||||
"app_survey_description": "將問卷嵌入您的 Web 應用程式或網站中以收集回應。",
|
||||
"assign": "等於 =",
|
||||
"audience": "受眾",
|
||||
@@ -1441,7 +1451,6 @@
|
||||
"follow_ups_modal_updated_successfull_toast": "後續 動作 已 更新 並 將 在 你 儲存 調查 後 儲存",
|
||||
"follow_ups_new": "新增後續追蹤",
|
||||
"follow_ups_upgrade_button_text": "升級以啟用後續追蹤",
|
||||
"form_styling": "表單樣式設定",
|
||||
"formbricks_sdk_is_not_connected": "Formbricks SDK 未連線",
|
||||
"four_points": "4 分",
|
||||
"heading": "標題",
|
||||
@@ -1531,7 +1540,7 @@
|
||||
"option_idx": "選項 '{'choiceIndex'}'",
|
||||
"option_used_in_logic_error": "此選項用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"optional": "選填",
|
||||
"options": "選項",
|
||||
"options": "選項*",
|
||||
"options_used_in_logic_bulk_error": "以下選項已用於邏輯中:{questionIndexes}。請先從邏輯中移除它們。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用此問卷的個別樣式覆寫主題。",
|
||||
"overwrite_global_waiting_time": "自訂冷卻期",
|
||||
@@ -1556,6 +1565,7 @@
|
||||
"question_deleted": "問題已刪除。",
|
||||
"question_duplicated": "問題已複製。",
|
||||
"question_id_updated": "問題 ID 已更新",
|
||||
"question_number": "第 {number} 題",
|
||||
"question_used_in_logic_warning_text": "此區塊中的元素已用於邏輯規則,確定要刪除嗎?",
|
||||
"question_used_in_logic_warning_title": "邏輯不一致",
|
||||
"question_used_in_quota": "此問題正被使用於「{quotaName}」配額中",
|
||||
@@ -1614,7 +1624,7 @@
|
||||
"response_limits_redirections_and_more": "回應限制、重新導向等。",
|
||||
"response_options": "回應選項",
|
||||
"roundness": "圓角",
|
||||
"roundness_description": "調整卡片邊角的圓弧度。",
|
||||
"roundness_description": "調整邊角的圓潤程度。",
|
||||
"row_used_in_logic_error": "此 row 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"rows": "列",
|
||||
"save_and_close": "儲存並關閉",
|
||||
@@ -1660,6 +1670,7 @@
|
||||
"survey_completed_subheading": "此免費且開源的問卷已關閉",
|
||||
"survey_display_settings": "問卷顯示設定",
|
||||
"survey_placement": "問卷位置",
|
||||
"survey_styling": "表單樣式設定",
|
||||
"survey_trigger": "問卷觸發器",
|
||||
"switch_multi_language_on_to_get_started": "請開啟多語言功能以開始使用 👉",
|
||||
"target_block_not_found": "找不到目標區塊",
|
||||
@@ -1748,9 +1759,9 @@
|
||||
"waiting_time_across_surveys": "冷卻期(跨問卷)",
|
||||
"waiting_time_across_surveys_description": "為避免問卷疲勞,請選擇此問卷如何與工作區的冷卻期互動。",
|
||||
"welcome_message": "歡迎訊息",
|
||||
"when": "當",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "如果沒有篩選器,則可以調查您的所有使用者。",
|
||||
"you_have_not_created_a_segment_yet": "您尚未建立區隔",
|
||||
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "您必須在工作區中設定兩種或以上語言,才能進行翻譯作業。",
|
||||
"your_description_here_recall_information_with": "您的描述在這裡。使用 @ 回憶資訊",
|
||||
"your_question_here_recall_information_with": "您的問題在這裡。使用 @ 回憶資訊",
|
||||
"your_web_app": "您的 Web 應用程式",
|
||||
@@ -2024,6 +2035,7 @@
|
||||
"starts": "開始次數",
|
||||
"starts_tooltip": "問卷已開始的次數。",
|
||||
"survey_reset_successfully": "調查 重置 成功!{responseCount} 條回應和 {displayCount} 個顯示被刪除。",
|
||||
"survey_results": "{surveyName} 結果",
|
||||
"this_month": "本月",
|
||||
"this_quarter": "本季",
|
||||
"this_year": "今年",
|
||||
@@ -2171,7 +2183,7 @@
|
||||
"advanced_styling_field_indicator_bg_description": "設定進度條已填滿部分的顏色。",
|
||||
"advanced_styling_field_input_border_radius_description": "調整輸入框的圓角。",
|
||||
"advanced_styling_field_input_font_size_description": "調整輸入框內輸入文字的大小。",
|
||||
"advanced_styling_field_input_height_description": "設定輸入欄位的最小高度。",
|
||||
"advanced_styling_field_input_height_description": "控制輸入欄的最小高度。",
|
||||
"advanced_styling_field_input_padding_x_description": "在左右兩側增加間距。",
|
||||
"advanced_styling_field_input_padding_y_description": "在上方和下方增加間距。",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "讓提示文字變得更淡。",
|
||||
@@ -2180,6 +2192,8 @@
|
||||
"advanced_styling_field_input_text_description": "設定輸入文字的顏色。",
|
||||
"advanced_styling_field_option_bg": "背景",
|
||||
"advanced_styling_field_option_bg_description": "填滿選項項目背景。",
|
||||
"advanced_styling_field_option_border": "邊框顏色",
|
||||
"advanced_styling_field_option_border_description": "為單選框和核取方塊選項加上外框。",
|
||||
"advanced_styling_field_option_border_radius_description": "讓選項的邊角變圓。",
|
||||
"advanced_styling_field_option_font_size_description": "調整選項標籤文字的大小。",
|
||||
"advanced_styling_field_option_label": "標籤顏色",
|
||||
@@ -2389,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "公司如何改善其願景和策略一致性?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "在此輸入您的答案...",
|
||||
"back": "返回",
|
||||
"block_1": "區塊 1",
|
||||
"block_10": "區塊 10",
|
||||
"block_2": "區塊 2",
|
||||
"block_3": "區塊 3",
|
||||
"block_4": "區塊 4",
|
||||
"block_5": "區塊 5",
|
||||
"block_6": "區塊 6",
|
||||
"block_7": "區塊 7",
|
||||
"block_8": "區塊 8",
|
||||
"block_9": "區塊 9",
|
||||
"book_interview": "預訂面試",
|
||||
"build_product_roadmap_description": "找出您的使用者最想要的一件事,然後建立它。",
|
||||
"build_product_roadmap_name": "建立產品路線圖",
|
||||
@@ -2596,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "唉,抱歉!我們是否有任何可以改善您體驗的地方?",
|
||||
"csat_survey_question_3_placeholder": "在此輸入您的答案...",
|
||||
"cta_description": "顯示資訊並提示使用者採取特定操作",
|
||||
"custom_survey_block_1_name": "區塊 1",
|
||||
"custom_survey_description": "建立沒有範本的問卷。",
|
||||
"custom_survey_name": "從頭開始",
|
||||
"custom_survey_question_1_headline": "您想瞭解什麼?",
|
||||
@@ -2999,6 +3022,9 @@
|
||||
"preview_survey_question_2_choice_2_label": "不用了,謝謝!",
|
||||
"preview_survey_question_2_headline": "想要緊跟最新動態嗎?",
|
||||
"preview_survey_question_2_subheader": "這是一個範例說明。",
|
||||
"preview_survey_question_open_text_headline": "還有什麼想和我們分享的嗎?",
|
||||
"preview_survey_question_open_text_placeholder": "在此輸入您的答案...",
|
||||
"preview_survey_question_open_text_subheader": "您的回饋能幫助我們進步。",
|
||||
"preview_survey_welcome_card_headline": "歡迎!",
|
||||
"prioritize_features_description": "找出您的使用者最需要和最不需要的功能。",
|
||||
"prioritize_features_name": "優先排序功能",
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export const SingleResponseCardBody = ({
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="mr-0.5 ml-0.5 rounded-md border border-slate-200 bg-slate-50 px-1 py-0.5 text-sm first:ml-0">
|
||||
className="ml-0.5 mr-0.5 rounded-md border border-slate-200 bg-slate-50 px-1 py-0.5 text-sm first:ml-0">
|
||||
@{part}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
import {
|
||||
mockedPrismaWebhookUpdateReturn,
|
||||
prismaNotFoundError,
|
||||
@@ -18,6 +20,10 @@ vi.mock("@formbricks/database", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/validate-webhook-url", () => ({
|
||||
validateWebhookUrl: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
describe("getWebhook", () => {
|
||||
test("returns ok if webhook is found", async () => {
|
||||
vi.mocked(prisma.webhook.findUnique).mockResolvedValueOnce({ id: "123" });
|
||||
@@ -63,6 +69,44 @@ describe("updateWebhook", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("calls validateWebhookUrl when URL is provided", async () => {
|
||||
vi.mocked(prisma.webhook.update).mockResolvedValueOnce(mockedPrismaWebhookUpdateReturn);
|
||||
|
||||
await updateWebhook("123", mockedWebhookUpdateReturn);
|
||||
|
||||
expect(validateWebhookUrl).toHaveBeenCalledWith("https://example.com");
|
||||
});
|
||||
|
||||
test("returns bad_request and skips Prisma update when URL fails SSRF validation", async () => {
|
||||
vi.mocked(validateWebhookUrl).mockRejectedValueOnce(
|
||||
new InvalidInputError("Webhook URL must not point to private or internal IP addresses")
|
||||
);
|
||||
|
||||
const result = await updateWebhook("123", mockedWebhookUpdateReturn);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.type).toBe("bad_request");
|
||||
expect(result.error.details[0].field).toBe("url");
|
||||
}
|
||||
|
||||
expect(prisma.webhook.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns internal_server_error when validateWebhookUrl throws an unexpected error", async () => {
|
||||
vi.mocked(validateWebhookUrl).mockRejectedValueOnce(new Error("unexpected DNS failure"));
|
||||
|
||||
const result = await updateWebhook("123", mockedWebhookUpdateReturn);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.type).toBe("internal_server_error");
|
||||
expect(result.error.details[0].field).toBe("url");
|
||||
}
|
||||
|
||||
expect(prisma.webhook.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns not_found if record does not exist", async () => {
|
||||
vi.mocked(prisma.webhook.update).mockRejectedValueOnce(prismaNotFoundError);
|
||||
const result = await updateWebhook("999", mockedWebhookUpdateReturn);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
import { ZWebhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
|
||||
@@ -34,6 +36,23 @@ export const updateWebhook = async (
|
||||
webhookId: string,
|
||||
webhookInput: z.infer<typeof ZWebhookUpdateSchema>
|
||||
): Promise<Result<Webhook, ApiErrorResponseV2>> => {
|
||||
if (webhookInput.url) {
|
||||
try {
|
||||
await validateWebhookUrl(webhookInput.url);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return err({
|
||||
type: "bad_request",
|
||||
details: [{ field: "url", issue: error.message }],
|
||||
});
|
||||
}
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "url", issue: "Webhook URL validation failed unexpectedly" }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedWebhook = await prisma.webhook.update({
|
||||
where: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { WebhookSource } from "@prisma/client";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
|
||||
import { createWebhook, getWebhooks } from "../webhook";
|
||||
|
||||
@@ -15,6 +17,10 @@ vi.mock("@formbricks/database", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/validate-webhook-url", () => ({
|
||||
validateWebhookUrl: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
describe("getWebhooks", () => {
|
||||
const environmentId = "env1";
|
||||
const params = {
|
||||
@@ -89,6 +95,44 @@ describe("createWebhook", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("calls validateWebhookUrl with the provided URL", async () => {
|
||||
vi.mocked(prisma.webhook.create).mockResolvedValueOnce(createdWebhook);
|
||||
|
||||
await createWebhook(inputWebhook);
|
||||
|
||||
expect(validateWebhookUrl).toHaveBeenCalledWith("http://example.com");
|
||||
});
|
||||
|
||||
test("returns bad_request and skips Prisma create when URL fails SSRF validation", async () => {
|
||||
vi.mocked(validateWebhookUrl).mockRejectedValueOnce(
|
||||
new InvalidInputError("Webhook URL must not point to private or internal IP addresses")
|
||||
);
|
||||
|
||||
const result = await createWebhook(inputWebhook);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.type).toEqual("bad_request");
|
||||
expect(result.error.details[0].field).toEqual("url");
|
||||
}
|
||||
|
||||
expect(prisma.webhook.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns internal_server_error when validateWebhookUrl throws an unexpected error", async () => {
|
||||
vi.mocked(validateWebhookUrl).mockRejectedValueOnce(new Error("unexpected DNS failure"));
|
||||
|
||||
const result = await createWebhook(inputWebhook);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
if (!result.ok) {
|
||||
expect(result.error.type).toEqual("internal_server_error");
|
||||
expect(result.error.details[0].field).toEqual("url");
|
||||
}
|
||||
|
||||
expect(prisma.webhook.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns error when creation fails", async () => {
|
||||
vi.mocked(prisma.webhook.create).mockRejectedValueOnce(new Error("Creation failed"));
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { generateWebhookSecret } from "@/lib/crypto";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
import { getWebhooksQuery } from "@/modules/api/v2/management/webhooks/lib/utils";
|
||||
import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
@@ -49,6 +51,21 @@ export const getWebhooks = async (
|
||||
export const createWebhook = async (webhook: TWebhookInput): Promise<Result<Webhook, ApiErrorResponseV2>> => {
|
||||
const { environmentId, name, url, source, triggers, surveyIds } = webhook;
|
||||
|
||||
try {
|
||||
await validateWebhookUrl(url);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return err({
|
||||
type: "bad_request",
|
||||
details: [{ field: "url", issue: error.message }],
|
||||
});
|
||||
}
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "url", issue: "Webhook URL validation failed unexpectedly" }],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const secret = generateWebhookSecret();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const EmailChangeWithoutVerificationSuccessPage = async () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">
|
||||
<div className="flex min-h-screen bg-gradient-radial from-slate-200 to-slate-50">
|
||||
<FormWrapper>
|
||||
<h1 className="leading-2 mb-4 text-center font-bold">
|
||||
{t("auth.email-change.email_change_success")}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const ForgotPasswordForm = () => {
|
||||
onChange={(e) => field.onChange(e)}
|
||||
autoComplete="email"
|
||||
required
|
||||
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
|
||||
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Invite } from "@prisma/client";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
export interface InviteWithCreator
|
||||
extends Pick<Invite, "id" | "expiresAt" | "organizationId" | "role" | "teamIds"> {
|
||||
export interface InviteWithCreator extends Pick<
|
||||
Invite,
|
||||
"id" | "expiresAt" | "organizationId" | "role" | "teamIds"
|
||||
> {
|
||||
creator: {
|
||||
name: string | null;
|
||||
email: string;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const AuthLayout = async ({ children }: { children: React.ReactNode }) =>
|
||||
<Toaster />
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
<div className="isolate bg-white">
|
||||
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">{children}</div>
|
||||
<div className="flex min-h-screen bg-gradient-radial from-slate-200 to-slate-50">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -195,6 +195,7 @@ describe("authOptions", () => {
|
||||
vi.mocked(applyIPRateLimit).mockRejectedValue(
|
||||
new Error("Maximum number of requests reached. Please try again later.")
|
||||
);
|
||||
const findUniqueSpy = vi.spyOn(prisma.user, "findUnique");
|
||||
|
||||
const credentials = { email: mockUser.email, password: mockPassword };
|
||||
|
||||
@@ -202,7 +203,7 @@ describe("authOptions", () => {
|
||||
"Maximum number of requests reached. Please try again later."
|
||||
);
|
||||
|
||||
expect(prisma.user.findUnique).not.toHaveBeenCalled();
|
||||
expect(findUniqueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use correct rate limit configuration", async () => {
|
||||
@@ -281,6 +282,7 @@ describe("authOptions", () => {
|
||||
vi.mocked(applyIPRateLimit).mockRejectedValue(
|
||||
new Error("Maximum number of requests reached. Please try again later.")
|
||||
);
|
||||
const findUniqueSpy = vi.spyOn(prisma.user, "findUnique");
|
||||
|
||||
const credentials = { token: "sometoken" };
|
||||
|
||||
@@ -288,7 +290,7 @@ describe("authOptions", () => {
|
||||
"Maximum number of requests reached. Please try again later."
|
||||
);
|
||||
|
||||
expect(prisma.user.findUnique).not.toHaveBeenCalled();
|
||||
expect(findUniqueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,7 +184,7 @@ export const LoginForm = ({
|
||||
value={field.value}
|
||||
onChange={(email) => field.onChange(email)}
|
||||
placeholder="work@email.com"
|
||||
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
|
||||
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
|
||||
/>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</div>
|
||||
@@ -207,7 +207,7 @@ export const LoginForm = ({
|
||||
aria-label="password"
|
||||
aria-required="true"
|
||||
required
|
||||
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 pr-8 shadow-sm sm:text-sm"
|
||||
className="block w-full rounded-md border-slate-300 pr-8 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
|
||||
value={field.value}
|
||||
onChange={(password) => field.onChange(password)}
|
||||
/>
|
||||
@@ -221,7 +221,7 @@ export const LoginForm = ({
|
||||
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="hover:text-brand-dark text-xs text-slate-500">
|
||||
className="text-xs text-slate-500 hover:text-brand-dark">
|
||||
{t("auth.login.forgot_your_password")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -222,7 +222,7 @@ export const SignupForm = ({
|
||||
placeholder="*******"
|
||||
aria-placeholder="password"
|
||||
required
|
||||
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md shadow-sm sm:text-sm"
|
||||
className="block w-full rounded-md shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
|
||||
/>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ export const VerifyEmailChangePage = async ({ searchParams }) => {
|
||||
const { token } = await searchParams;
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">
|
||||
<div className="flex min-h-screen bg-gradient-radial from-slate-200 to-slate-50">
|
||||
<FormWrapper>
|
||||
<EmailChangeSignIn token={token} />
|
||||
<BackToLoginButton />
|
||||
|
||||
@@ -138,7 +138,7 @@ export const PricingTable = ({
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full">
|
||||
<h2 className="mr-2 mb-3 inline-flex w-full text-2xl font-bold text-slate-700">
|
||||
<h2 className="mb-3 mr-2 inline-flex w-full text-2xl font-bold text-slate-700">
|
||||
{t("environments.settings.billing.current_plan")}:{" "}
|
||||
<span className="capitalize">{organization.billing.plan}</span>
|
||||
{cancellingOn && (
|
||||
@@ -203,7 +203,7 @@ export const PricingTable = ({
|
||||
<div
|
||||
className={cn(
|
||||
"relative mx-8 mb-8 flex flex-col gap-4",
|
||||
peopleUnlimitedCheck && "mt-4 mb-0 flex-row pb-0"
|
||||
peopleUnlimitedCheck && "mb-0 mt-4 flex-row pb-0"
|
||||
)}>
|
||||
<p className="text-md font-semibold text-slate-700">
|
||||
{t("environments.settings.billing.monthly_identified_users")}
|
||||
@@ -226,7 +226,7 @@ export const PricingTable = ({
|
||||
<div
|
||||
className={cn(
|
||||
"relative mx-8 flex flex-col gap-4 pb-6",
|
||||
projectsUnlimitedCheck && "mt-4 mb-0 flex-row pb-0"
|
||||
projectsUnlimitedCheck && "mb-0 mt-4 flex-row pb-0"
|
||||
)}>
|
||||
<p className="text-md font-semibold text-slate-700">{t("common.workspaces")}</p>
|
||||
{organization.billing.limits.projects && (
|
||||
@@ -264,7 +264,7 @@ export const PricingTable = ({
|
||||
</button>
|
||||
<button
|
||||
aria-pressed={planPeriod === "yearly"}
|
||||
className={`flex-1 items-center rounded-md py-0.5 pr-2 pl-4 text-center whitespace-nowrap ${
|
||||
className={`flex-1 items-center whitespace-nowrap rounded-md py-0.5 pl-4 pr-2 text-center ${
|
||||
planPeriod === "yearly" ? "bg-slate-200 font-semibold" : "bg-transparent"
|
||||
}`}
|
||||
onClick={() => handleMonthlyToggle("yearly")}>
|
||||
@@ -276,7 +276,7 @@ export const PricingTable = ({
|
||||
</div>
|
||||
<div className="relative mx-auto grid max-w-md grid-cols-1 gap-y-8 lg:mx-0 lg:-mb-14 lg:max-w-none lg:grid-cols-3">
|
||||
<div
|
||||
className="hidden lg:absolute lg:inset-x-px lg:top-4 lg:bottom-0 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
|
||||
className="hidden lg:absolute lg:inset-x-px lg:bottom-0 lg:top-4 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{getCloudPricingData(t).plans.map((plan) => (
|
||||
|
||||
@@ -98,7 +98,7 @@ export const ActivityTimeline = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSort}
|
||||
className="hover:text-brand-dark flex items-center px-1 text-slate-800">
|
||||
className="flex items-center px-1 text-slate-800 hover:text-brand-dark">
|
||||
<ArrowDownUpIcon className="inline h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ContactControlBar = ({
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
setOpen={setDeleteDialogOpen}
|
||||
deleteWhat="person"
|
||||
deleteWhat={t("common.person")}
|
||||
onDelete={handleDeletePerson}
|
||||
isDeleting={isDeletingPerson}
|
||||
text={
|
||||
|
||||
@@ -66,7 +66,7 @@ export const DeleteContactButton = ({
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
setOpen={setDeleteDialogOpen}
|
||||
deleteWhat="person"
|
||||
deleteWhat={t("common.person")}
|
||||
onDelete={handleDeletePerson}
|
||||
isDeleting={isDeletingPerson}
|
||||
text={
|
||||
|
||||
@@ -52,54 +52,41 @@ export const getPersonSegmentIds = async (
|
||||
return [];
|
||||
}
|
||||
|
||||
// Phase 1: Build all Prisma where clauses concurrently.
|
||||
// This converts segment filters into where clauses without per-contact DB queries.
|
||||
const segmentWithClauses = await Promise.all(
|
||||
segments.map(async (segment) => {
|
||||
const filters = segment.filters as TBaseFilters | null;
|
||||
|
||||
if (!filters || filters.length === 0) {
|
||||
return { segmentId: segment.id, whereClause: {} as Prisma.ContactWhereInput };
|
||||
}
|
||||
|
||||
const queryResult = await segmentFilterToPrismaQuery(segment.id, filters, environmentId, deviceType);
|
||||
|
||||
if (!queryResult.ok) {
|
||||
logger.warn(
|
||||
{ segmentId: segment.id, environmentId, error: queryResult.error },
|
||||
"Failed to build Prisma query for segment"
|
||||
);
|
||||
return { segmentId: segment.id, whereClause: null };
|
||||
}
|
||||
|
||||
return { segmentId: segment.id, whereClause: queryResult.data.whereClause };
|
||||
})
|
||||
);
|
||||
|
||||
// Separate segments into: always-match (no filters), needs-DB-check, and failed-to-build
|
||||
// Phase 1: Build WHERE clauses sequentially to avoid connection pool contention.
|
||||
// segmentFilterToPrismaQuery can itself hit the DB (e.g. unmigrated-row checks),
|
||||
// so running all builds concurrently would saturate the pool.
|
||||
const alwaysMatchIds: string[] = [];
|
||||
const toCheck: { segmentId: string; whereClause: Prisma.ContactWhereInput }[] = [];
|
||||
const dbChecks: { segmentId: string; whereClause: Prisma.ContactWhereInput }[] = [];
|
||||
|
||||
for (const item of segmentWithClauses) {
|
||||
if (item.whereClause === null) {
|
||||
for (const segment of segments) {
|
||||
const filters = segment.filters as TBaseFilters;
|
||||
|
||||
if (!filters?.length) {
|
||||
alwaysMatchIds.push(segment.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Object.keys(item.whereClause).length === 0) {
|
||||
alwaysMatchIds.push(item.segmentId);
|
||||
} else {
|
||||
toCheck.push({ segmentId: item.segmentId, whereClause: item.whereClause });
|
||||
const queryResult = await segmentFilterToPrismaQuery(segment.id, filters, environmentId, deviceType);
|
||||
|
||||
if (!queryResult.ok) {
|
||||
logger.warn(
|
||||
{ segmentId: segment.id, environmentId, error: queryResult.error },
|
||||
"Failed to build Prisma query for segment, skipping"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
dbChecks.push({ segmentId: segment.id, whereClause: queryResult.data.whereClause });
|
||||
}
|
||||
|
||||
if (toCheck.length === 0) {
|
||||
if (dbChecks.length === 0) {
|
||||
return alwaysMatchIds;
|
||||
}
|
||||
|
||||
// Phase 2: Batch all contact-match checks into a single DB transaction.
|
||||
// Replaces N individual findFirst queries with one batched round-trip.
|
||||
const batchResults = await prisma.$transaction(
|
||||
toCheck.map(({ whereClause }) =>
|
||||
// Phase 2: Execute all membership checks in a single transaction.
|
||||
// Uses one connection instead of N concurrent ones, eliminating pool contention.
|
||||
const txResults = await prisma.$transaction(
|
||||
dbChecks.map(({ whereClause }) =>
|
||||
prisma.contact.findFirst({
|
||||
where: { id: contactId, ...whereClause },
|
||||
select: { id: true },
|
||||
@@ -107,17 +94,12 @@ export const getPersonSegmentIds = async (
|
||||
)
|
||||
);
|
||||
|
||||
// Phase 3: Collect matching segment IDs
|
||||
const dbMatchIds = toCheck.filter((_, i) => batchResults[i] !== null).map(({ segmentId }) => segmentId);
|
||||
const matchedIds = dbChecks.filter((_, i) => txResults[i] !== null).map(({ segmentId }) => segmentId);
|
||||
|
||||
return [...alwaysMatchIds, ...dbMatchIds];
|
||||
return [...alwaysMatchIds, ...matchedIds];
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
{
|
||||
environmentId,
|
||||
contactId,
|
||||
error,
|
||||
},
|
||||
{ environmentId, contactId, error },
|
||||
"Failed to get person segment IDs, returning empty array"
|
||||
);
|
||||
return [];
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TJsPersonState } from "@formbricks/types/js";
|
||||
import { getPersonSegmentIds } from "./segments";
|
||||
import { getUserState } from "./user-state";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
contact: {
|
||||
findUniqueOrThrow: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./segments", () => ({
|
||||
getPersonSegmentIds: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockEnvironmentId = "test-environment-id";
|
||||
const mockUserId = "test-user-id";
|
||||
const mockContactId = "test-contact-id";
|
||||
const mockDevice = "desktop";
|
||||
|
||||
describe("getUserState", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should return user state with empty responses and displays", async () => {
|
||||
const mockContactData = {
|
||||
id: mockContactId,
|
||||
responses: [],
|
||||
displays: [],
|
||||
};
|
||||
vi.mocked(prisma.contact.findUniqueOrThrow).mockResolvedValue(mockContactData as any);
|
||||
vi.mocked(getPersonSegmentIds).mockResolvedValue(["segment1"]);
|
||||
|
||||
const result = await getUserState({
|
||||
environmentId: mockEnvironmentId,
|
||||
userId: mockUserId,
|
||||
contactId: mockContactId,
|
||||
device: mockDevice,
|
||||
});
|
||||
|
||||
expect(prisma.contact.findUniqueOrThrow).toHaveBeenCalledWith({
|
||||
where: { id: mockContactId },
|
||||
select: {
|
||||
id: true,
|
||||
responses: {
|
||||
select: { surveyId: true },
|
||||
},
|
||||
displays: {
|
||||
select: { surveyId: true, createdAt: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getPersonSegmentIds).toHaveBeenCalledWith(
|
||||
mockEnvironmentId,
|
||||
mockContactId,
|
||||
mockUserId,
|
||||
mockDevice
|
||||
);
|
||||
expect(result).toEqual<TJsPersonState["data"]>({
|
||||
contactId: mockContactId,
|
||||
userId: mockUserId,
|
||||
segments: ["segment1"],
|
||||
displays: [],
|
||||
responses: [],
|
||||
lastDisplayAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("should return user state with responses and displays, and sort displays by createdAt", async () => {
|
||||
const mockDate1 = new Date("2023-01-01T00:00:00.000Z");
|
||||
const mockDate2 = new Date("2023-01-02T00:00:00.000Z");
|
||||
|
||||
const mockContactData = {
|
||||
id: mockContactId,
|
||||
responses: [{ surveyId: "survey1" }, { surveyId: "survey2" }],
|
||||
displays: [
|
||||
{ surveyId: "survey4", createdAt: mockDate2 }, // most recent (already sorted by desc)
|
||||
{ surveyId: "survey3", createdAt: mockDate1 },
|
||||
],
|
||||
};
|
||||
vi.mocked(prisma.contact.findUniqueOrThrow).mockResolvedValue(mockContactData as any);
|
||||
vi.mocked(getPersonSegmentIds).mockResolvedValue(["segment2", "segment3"]);
|
||||
|
||||
const result = await getUserState({
|
||||
environmentId: mockEnvironmentId,
|
||||
userId: mockUserId,
|
||||
contactId: mockContactId,
|
||||
device: mockDevice,
|
||||
});
|
||||
|
||||
expect(result).toEqual<TJsPersonState["data"]>({
|
||||
contactId: mockContactId,
|
||||
userId: mockUserId,
|
||||
segments: ["segment2", "segment3"],
|
||||
displays: [
|
||||
{ surveyId: "survey4", createdAt: mockDate2 },
|
||||
{ surveyId: "survey3", createdAt: mockDate1 },
|
||||
],
|
||||
responses: ["survey1", "survey2"],
|
||||
lastDisplayAt: mockDate2,
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle empty arrays from prisma", async () => {
|
||||
// This case tests with proper empty arrays instead of null
|
||||
const mockContactData = {
|
||||
id: mockContactId,
|
||||
responses: [],
|
||||
displays: [],
|
||||
};
|
||||
vi.mocked(prisma.contact.findUniqueOrThrow).mockResolvedValue(mockContactData as any);
|
||||
vi.mocked(getPersonSegmentIds).mockResolvedValue([]);
|
||||
|
||||
const result = await getUserState({
|
||||
environmentId: mockEnvironmentId,
|
||||
userId: mockUserId,
|
||||
contactId: mockContactId,
|
||||
device: mockDevice,
|
||||
});
|
||||
|
||||
expect(result).toEqual<TJsPersonState["data"]>({
|
||||
contactId: mockContactId,
|
||||
userId: mockUserId,
|
||||
segments: [],
|
||||
displays: [],
|
||||
responses: [],
|
||||
lastDisplayAt: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TJsPersonState } from "@formbricks/types/js";
|
||||
import { getPersonSegmentIds } from "./segments";
|
||||
|
||||
/**
|
||||
* Optimized single query to get all user state data
|
||||
* Replaces multiple separate queries with one efficient query
|
||||
*/
|
||||
const getUserStateDataOptimized = async (contactId: string) => {
|
||||
return prisma.contact.findUniqueOrThrow({
|
||||
where: { id: contactId },
|
||||
select: {
|
||||
id: true,
|
||||
responses: {
|
||||
select: { surveyId: true },
|
||||
},
|
||||
displays: {
|
||||
select: {
|
||||
surveyId: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Optimized user state fetcher without caching
|
||||
* Uses single database query and efficient data processing
|
||||
* NO CACHING - user state changes frequently with contact updates
|
||||
*
|
||||
* @param environmentId - The environment id
|
||||
* @param userId - The user id
|
||||
* @param device - The device type
|
||||
* @returns The person state
|
||||
* @throws {ValidationError} - If the input is invalid
|
||||
* @throws {ResourceNotFoundError} - If the environment or organization is not found
|
||||
*/
|
||||
export const getUserState = async ({
|
||||
environmentId,
|
||||
userId,
|
||||
contactId,
|
||||
device,
|
||||
}: {
|
||||
environmentId: string;
|
||||
userId: string;
|
||||
contactId: string;
|
||||
device: "phone" | "desktop";
|
||||
}): Promise<TJsPersonState["data"]> => {
|
||||
// Single optimized query for all contact data
|
||||
const contactData = await getUserStateDataOptimized(contactId);
|
||||
|
||||
// Get segments using Prisma-based evaluation (no attributes needed - fetched from DB)
|
||||
const segments = await getPersonSegmentIds(environmentId, contactId, userId, device);
|
||||
|
||||
// Process displays efficiently
|
||||
const displays = (contactData.displays ?? []).map((display) => ({
|
||||
surveyId: display.surveyId,
|
||||
createdAt: display.createdAt,
|
||||
}));
|
||||
|
||||
// Get latest display date
|
||||
const lastDisplayAt =
|
||||
contactData.displays && contactData.displays.length > 0 ? contactData.displays[0].createdAt : null;
|
||||
|
||||
// Process responses efficiently
|
||||
const responses = (contactData.responses ?? []).map((response) => response.surveyId);
|
||||
|
||||
const userState: TJsPersonState["data"] = {
|
||||
contactId,
|
||||
userId,
|
||||
segments,
|
||||
displays,
|
||||
responses,
|
||||
lastDisplayAt,
|
||||
};
|
||||
|
||||
return userState;
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export const generateAttributeTableColumns = (
|
||||
cell: ({ row }) => {
|
||||
const description = row.original.description;
|
||||
return description ? (
|
||||
<div className={isExpanded ? "break-words whitespace-normal" : "truncate"}>
|
||||
<div className={isExpanded ? "whitespace-normal break-words" : "truncate"}>
|
||||
<HighlightedText value={description} searchValue={searchValue} />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getLocale } from "@/lingodotdev/language";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { ContactsPageLayout } from "@/modules/ee/contacts/components/contacts-page-layout";
|
||||
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -13,7 +14,7 @@ export const AttributesPage = async ({
|
||||
}) => {
|
||||
const params = await paramsProps;
|
||||
const locale = await getLocale();
|
||||
|
||||
const t = await getTranslate();
|
||||
const [{ isReadOnly }, contactAttributeKeys] = await Promise.all([
|
||||
getEnvironmentAuth(params.environmentId),
|
||||
getContactAttributeKeys(params.environmentId),
|
||||
@@ -23,7 +24,7 @@ export const AttributesPage = async ({
|
||||
|
||||
return (
|
||||
<ContactsPageLayout
|
||||
pageTitle="Contacts"
|
||||
pageTitle={t("common.contacts")}
|
||||
activeId="attributes"
|
||||
environmentId={params.environmentId}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { TFunction } from "i18next";
|
||||
import { formatAttributeValue } from "@/modules/ee/contacts/lib/format-attribute-value";
|
||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||
import { HighlightedText } from "@/modules/ui/components/highlighted-text";
|
||||
@@ -10,12 +11,13 @@ import { TContactTableData } from "../types/contact";
|
||||
export const generateContactTableColumns = (
|
||||
searchValue: string,
|
||||
data: TContactTableData[],
|
||||
isReadOnly: boolean
|
||||
isReadOnly: boolean,
|
||||
t: TFunction
|
||||
): ColumnDef<TContactTableData>[] => {
|
||||
const userColumn: ColumnDef<TContactTableData> = {
|
||||
id: "contactsTableUser",
|
||||
accessorKey: "contactsTableUser",
|
||||
header: "ID",
|
||||
header: t("common.id"),
|
||||
cell: ({ row }) => {
|
||||
const contactId = row.original.id;
|
||||
return <HighlightedText value={contactId} searchValue={searchValue} />;
|
||||
@@ -25,7 +27,7 @@ export const generateContactTableColumns = (
|
||||
const userIdColumn: ColumnDef<TContactTableData> = {
|
||||
id: "userId",
|
||||
accessorKey: "userId",
|
||||
header: "User ID",
|
||||
header: t("common.user_id"),
|
||||
cell: ({ row }) => {
|
||||
const userId = row.original.userId;
|
||||
return <IdBadge id={userId} />;
|
||||
@@ -35,7 +37,7 @@ export const generateContactTableColumns = (
|
||||
const emailColumn: ColumnDef<TContactTableData> = {
|
||||
id: "email",
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
header: t("common.email"),
|
||||
cell: ({ row }) => {
|
||||
const email = row.original.email;
|
||||
if (email) {
|
||||
@@ -47,7 +49,7 @@ export const generateContactTableColumns = (
|
||||
const firstNameColumn: ColumnDef<TContactTableData> = {
|
||||
id: "firstName",
|
||||
accessorKey: "firstName",
|
||||
header: "First Name",
|
||||
header: t("common.first_name"),
|
||||
cell: ({ row }) => {
|
||||
const firstName = row.original.firstName;
|
||||
return <HighlightedText value={firstName} searchValue={searchValue} />;
|
||||
@@ -57,7 +59,7 @@ export const generateContactTableColumns = (
|
||||
const lastNameColumn: ColumnDef<TContactTableData> = {
|
||||
id: "lastName",
|
||||
accessorKey: "lastName",
|
||||
header: "Last Name",
|
||||
header: t("common.last_name"),
|
||||
cell: ({ row }) => {
|
||||
const lastName = row.original.lastName;
|
||||
return <HighlightedText value={lastName} searchValue={searchValue} />;
|
||||
|
||||
@@ -71,7 +71,7 @@ export const ContactsTable = ({
|
||||
|
||||
// Generate columns
|
||||
const columns = useMemo(() => {
|
||||
return generateContactTableColumns(searchValue, data, isReadOnly);
|
||||
return generateContactTableColumns(searchValue, data, isReadOnly, t);
|
||||
}, [searchValue, data, isReadOnly]);
|
||||
|
||||
// Load saved settings from localStorage
|
||||
|
||||
@@ -132,7 +132,7 @@ export const UploadContactsAttributes = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="overflow-hidden font-medium text-ellipsis text-slate-700">{csvColumn}</span>
|
||||
<span className="overflow-hidden text-ellipsis font-medium text-slate-700">{csvColumn}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<UploadContactsAttributeCombobox
|
||||
open={open}
|
||||
|
||||
@@ -682,10 +682,18 @@ export const createContactsFromCSV = async (
|
||||
environmentId,
|
||||
};
|
||||
|
||||
const contactPromises = csvData.map((record) => processCsvRecord(record, processingContext));
|
||||
const CHUNK_SIZE = 50;
|
||||
const allResults: (TContact | null)[] = [];
|
||||
|
||||
const results = await Promise.all(contactPromises);
|
||||
return { contacts: results.filter((contact): contact is TContact => contact !== null) };
|
||||
for (let i = 0; i < csvData.length; i += CHUNK_SIZE) {
|
||||
const chunk = csvData.slice(i, i + CHUNK_SIZE);
|
||||
const chunkResults = await Promise.all(
|
||||
chunk.map((record) => processCsvRecord(record, processingContext))
|
||||
);
|
||||
allResults.push(...chunkResults);
|
||||
}
|
||||
|
||||
return { contacts: allResults.filter((contact): contact is TContact => contact !== null) };
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
|
||||
@@ -93,7 +93,7 @@ export const EditSegmentModal = ({
|
||||
key={tab.title}
|
||||
className={`mr-4 px-1 pb-3 focus:outline-none ${
|
||||
activeTab === index
|
||||
? "border-brand-dark border-b-2 font-semibold text-slate-900"
|
||||
? "border-b-2 border-brand-dark font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700"
|
||||
}`}
|
||||
onClick={() => handleTabClick(index)}>
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { TFunction } from "i18next";
|
||||
import { UsersIcon } from "lucide-react";
|
||||
import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
|
||||
export const generateSegmentTableColumns = (): ColumnDef<TSegmentWithSurveyNames>[] => {
|
||||
export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWithSurveyNames>[] => {
|
||||
const titleColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||
id: "title",
|
||||
accessorKey: "title",
|
||||
header: "Title",
|
||||
header: t("common.title"),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -30,7 +31,7 @@ export const generateSegmentTableColumns = (): ColumnDef<TSegmentWithSurveyNames
|
||||
const updatedAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||
id: "updatedAt",
|
||||
accessorKey: "updatedAt",
|
||||
header: "Updated",
|
||||
header: t("common.updated_at"),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="text-sm text-slate-900">
|
||||
@@ -43,7 +44,7 @@ export const generateSegmentTableColumns = (): ColumnDef<TSegmentWithSurveyNames
|
||||
const createdAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||
id: "createdAt",
|
||||
accessorKey: "createdAt",
|
||||
header: "Created",
|
||||
header: t("common.created_at"),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="text-sm text-slate-900">{format(row.original.createdAt, "do 'of' MMMM, yyyy")}</span>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const SegmentTableDataRow = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 my-auto hidden text-center text-sm whitespace-nowrap text-slate-500 sm:block">
|
||||
<div className="col-span-1 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{surveys?.length}</div>
|
||||
</div>
|
||||
<div className="whitespace-wrap col-span-1 my-auto hidden text-center text-sm text-slate-500 sm:block">
|
||||
@@ -52,7 +52,7 @@ export const SegmentTableDataRow = ({
|
||||
}).replace("about", "")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 my-auto hidden text-center text-sm whitespace-normal text-slate-500 sm:block">
|
||||
<div className="col-span-1 my-auto hidden whitespace-normal text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{format(createdAt, "do 'of' MMMM, yyyy")}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function SegmentTable({
|
||||
const [editingSegment, setEditingSegment] = useState<TSegmentWithSurveyNames | null>(null);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return generateSegmentTableColumns();
|
||||
return generateSegmentTableColumns(t);
|
||||
}, []);
|
||||
|
||||
const table = useReactTable({
|
||||
|
||||
@@ -176,7 +176,7 @@ export function TargetingCard({
|
||||
asChild
|
||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pr-5 pl-2">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
strokeWidth={3}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const SegmentsPage = async ({
|
||||
|
||||
return (
|
||||
<ContactsPageLayout
|
||||
pageTitle="Contacts"
|
||||
pageTitle={t("common.contacts")}
|
||||
activeId="segments"
|
||||
environmentId={params.environmentId}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
|
||||
@@ -249,7 +249,7 @@ export function EditLanguage({
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 italic">
|
||||
<p className="text-sm italic text-slate-500">
|
||||
{t("environments.workspace.languages.no_language_found")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user