Compare commits

..

42 Commits

Author SHA1 Message Date
Dhruwang a6b777db6f fix: translation 2026-03-10 11:27:57 +05:30
Balázs Úr 34c587342c update translatable keys 2026-03-06 12:19:45 +01:00
Balázs Úr 5a0b421153 merged main 2026-03-06 11:47:59 +01:00
Chowdhury Tafsir Ahmed Siddiki af02ce9ea6 fix: display native language names in profile language selector (#7349)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-03-06 10:18:52 +00:00
Bhagya Amarasinghe fc1c91896a fix: add server-side SSRF validation for webhook URLs (#7414)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-03-06 07:36:49 +00:00
Balázs Úr f5c7dbdc71 fix: mark duplicated survey name as translatable (#7379)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-03-06 06:37:05 +00:00
Balázs Úr b88ea5cc66 fix: use proper plural forms (#7322)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-03-06 06:30:27 +00:00
bharath kumar f31085a9e7 fix(i18n): resolve duplicate Hungarian translations causing Career Development Survey creation to fail (#7410)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-03-06 05:39:05 +00:00
Dhruwang Jariwala 2ab0441404 fix: z-index for multi select question with dropdwon display type (#7420) 2026-03-06 04:56:39 +00:00
Dhruwang Jariwala 299ae81b21 chore: mls tweaks (#7416) 2026-03-05 14:55:45 +00:00
Bhagya Amarasinghe f73f13f16c perf: fix Prisma connection pool saturation from unbounded Promise.all fan-outs (#7404) 2026-03-05 14:35:40 +00:00
Matti Nannt e9bcbf6e4c fix: patch @isaacs/brace-expansion to 5.0.1 (#7424) 2026-03-05 13:35:48 +00:00
Matti Nannt 32eda35a71 chore: clean up stale turbo task config (#7423) 2026-03-05 11:49:24 +00:00
Dhruwang Jariwala 84999cddfd feat: danish support to surveys package (#7415) 2026-03-05 11:05:40 +00:00
Matti Nannt f0a0cf531a chore: clean up unused npm dependencies (#7417)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-03-05 10:48:13 +00:00
Matti Nannt f3e02fa466 chore: optimize monorepo build performance (#7419) 2026-03-05 10:18:54 +00:00
Dhruwang Jariwala f0a93ae092 fix: add Tailwind v3 config for Prettier in apps/web and packages/email (#7421) 2026-03-05 10:05:05 +00:00
Matti Nannt 1c922dfe2c chore: remove legacy post-checkout hook (#7418) 2026-03-05 08:14:19 +00:00
Bhagya Amarasinghe 33010fb6f5 fix: auto-save creates duplicate follow ups (#7413)
Co-authored-by: gulshank0 <gulshanbahadur002@gmail.com>
2026-03-05 00:44:29 +00:00
Matti Nannt d5fdacadd7 chore: update dependencies and fix build/lint/test regressions (#7403) 2026-03-03 17:03:03 +00:00
bharath kumar d939263472 fix(sdk): add userId length limit to mitigate DoS attack risk (#7378)
Co-authored-by: Matti Nannt <matti@formbricks.com>
2026-03-03 10:10:01 +01:00
Dhruwang Jariwala e4aa66b067 fix: removed legacy response note traces (#7396) 2026-03-02 12:58:37 +00:00
Dhruwang Jariwala ffcc101ed9 chore: make productionBrowserSourceMaps conditional to decrease build time (#7400) 2026-03-02 09:49:00 +00:00
Balázs Úr 2740cd16b9 fix: delete confirmation dialog title translation (#7358)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-03-02 07:06:14 +00:00
Dhruwang Jariwala 7eb94f0bd5 fix: theme styling preview, option border color, and enable custom styling behavior (#7387)
Co-authored-by: Johannes <johannes@formbricks.com>
2026-03-02 06:17:52 +00:00
Johannes 6dd2e707fe feat: display Formbricks version alongside organization ID in settings (#7363) 2026-03-02 05:54:23 +00:00
Matti Nannt 58d5de7d45 fix: resolve Dependabot Next.js deserialization alert (#7393) 2026-02-27 22:18:38 +01:00
Dhruwang Jariwala 7c3fa8b5ea fix: restore bullet points in survey preview and public survey (#7356) (#7360)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-27 18:24:15 +00:00
Harsh Bhat 2601169877 docs: add advanced CSS variable updates (#7389)
Co-authored-by: Johannes <johannes@formbricks.com>
2026-02-27 17:19:22 +00:00
bharath kumar aecf85815a fix(js-core): use closest() fallback for nested click target matching (#7327)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-02-27 06:24:58 +00:00
Bhagya Amarasinghe c6ebaea989 fix: set success_action_status on S3 presigned POST to fix CORS on Ceph-based providers (#7362) 2026-02-26 10:26:49 +00:00
Bhagya Amarasinghe 68c1422733 fix: copy database package.json to Docker runner stage (#7371) 2026-02-26 10:25:28 +00:00
Dhruwang Jariwala 6942502baf fix: slack missing redirect uri (#7372) 2026-02-26 10:01:25 +00:00
Balázs Úr f6fab9a996 fix connector rendering 2026-02-26 09:39:15 +01:00
Balázs Úr fe33527da8 fix: mark strings as translatable in survey editor 2026-02-26 08:29:05 +01:00
Theodór Tómas a4bd217761 chore: update to zod 3.25.76 (#7366) 2026-02-26 05:17:20 +00:00
Bhagya Amarasinghe fee770358c perf(contacts): build segment WHERE clauses sequentially to prevent pool saturation (#7354)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-02-25 15:25:32 +00:00
Dhruwang Jariwala 44f8f80cac docs: clarify startAt is block-based, not question-based (#1404) (#7352)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 13:19:30 +00:00
Chowdhury Tafsir Ahmed Siddiki 858a7f7aa9 fix: replace toSorted in breadcrumb switchers for compatibility (#7325)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-02-25 06:29:31 +00:00
Gulshan ac40b90e81 fix: made "Filter" string translatable (#7301)
Signed-off-by: gulshank0 <gulshanbahadur002@gmail.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-02-25 06:28:51 +00:00
Balázs Úr aa21b4e442 fix: made Contact's page titles and table headers translatable (#7313)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-02-24 14:07:05 +00:00
Dhruwang Jariwala fa72296de5 fix: error state for multi select question (#7335) 2026-02-24 13:34:48 +00:00
268 changed files with 8420 additions and 8714 deletions
-2
View File
@@ -1,2 +0,0 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json
prettier --write ./branch.json
+12 -17
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
const baseConfig = require("../../.prettierrc.js");
module.exports = {
...baseConfig,
tailwindConfig: "./tailwind.config.js",
};
+3
View File
@@ -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
@@ -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`}>
@@ -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")} />
@@ -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={"/"}>
@@ -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={"/"}>
@@ -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}
@@ -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} />
@@ -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 />
@@ -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
@@ -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 && (
@@ -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)}
/>
@@ -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,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 {
@@ -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} />
@@ -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">
@@ -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}
@@ -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>
@@ -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,
@@ -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)}%
@@ -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,
@@ -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)}%
@@ -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>
@@ -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",
},
]}
/>
@@ -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}
@@ -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`}
@@ -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
@@ -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>
@@ -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>
+12 -7
View File
@@ -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") {
+112 -114
View File
@@ -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();
+30 -45
View File
@@ -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 });
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
View File
@@ -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
+2 -1
View File
@@ -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;
+48 -26
View File
@@ -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");
});
});
+12 -2
View File
@@ -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
+18
View File
@@ -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"])
);
-1
View File
@@ -397,7 +397,6 @@ export const getResponseDownloadFile = async (
"Survey ID",
"Formbricks ID (internal)",
"User ID",
"Notes",
"Tags",
...metaDataFields,
...elements.flat(),
+6 -2
View File
@@ -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"] },
+1
View File
@@ -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",
});
});
});
});
+176
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+39 -13
View File
@@ -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": "优先 功能",
+38 -12
View File
@@ -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": "優先排序功能",
@@ -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;
+1 -1
View File
@@ -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 [];
@@ -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}
+11 -3
View File
@@ -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